Reddit++

Improved experience for reddit.com

当前为 2024-03-19 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Reddit++
// @name:ru      Reddit++
// @namespace    RedditPlusPlus
// @license      CC-BY-SA-4.0
// @version      0.1.7
// @description  Improved experience for reddit.com
// @description:ru  Улучшение интерфейса reddit.com
// @author       lnm95
// @match        *://www.reddit.com/*
// @icon         data:image/gif;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEf/NABE//QARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//QBE/+8ARP/MAEP/ngBF/1wAIv8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9OAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//gAR/+gAEj/JQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/7AFP/kABI/woAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/1wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQv+YAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wA9/zQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACL/JABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8BRf//E1L9/yVe/P8xZ/v/N2r6/zdq+v8wZvr/Jl77/xNR/f8BRv7/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wAj/yIAAAAAAAAAAAAAAAAAAAAAAAAAAABI/woARf/XAEX//wBF//8ARf//AEX//wBF//8GSf3/K2H3/0l28v9Re+//UXru/1B67f9Qee3/UHnt/1B67f9Qeu7/UXrv/0p28f8pYPf/Bkn9/wBF//8ARf//AEX//wBF//8ARf//AEX/2ABG/wgAAAAAAAAAAAAAAAAAAAAAAFP/jwBF//8ARf//AEX//wBF//8ARf//HFf4/0p17f9Qeez/UHns/1B56/9Qeez/UHrs/1J87/9SfO//UHrt/1B57P9Qeez/UHnr/1B57P9Lde3/HVj4/wBF//8ARf//AEX//wBF//8ARf//AEX/kQAAAAAAAAAAAAAAAABH/yUARf/7AEX//wBF//8ARf//AEX//ydf9f9Qeev/UHrs/1F77v9TfO//U37w/0Zx5f8iTcD/Ejyt/xE7rP8hTL//RXDk/1R+8f9TffD/Unzv/1F77f9Qeez/KF/1/wBF//8ARf//AEX//wBF//8ARf/5AEX/LAAAAAAAAAAAAEP/oABF//8ARf//AEX//wBF//8dWPj/UXvt/1N98P9Uf/P/VoH0/1aB9f9CbuL/CTSo/wgzpv8JNKf/CTSn/wczpv8JNKj/QW3i/1aC9v9WgfT/VYDz/1R/8f9Sfe//H1n4/wBF//8ARf//AEX//wBF//8ARf+gAAAAAAAi/xAARf/4AEX//wBF//8ARf//BEj+/0168v9VgPP/VoL1/1eC9v9Yg/j/WIP5/xA8sv8DLqT/BC+k/wUwpf8FMKX/BC+k/wMupP8PO7D/WIT5/1iD+P9Xg/f/V4L2/1aB9P9Oe/P/BEj+/wBF//8ARf//AEX//wBF//cARf8QAEX/XABF//8ARf//AEX//wBF//8fW/v/VoL1/1eD9/9Yg/j/x83h/+fi2P/Fyt3/VH/0/0x37P9Ic+j/RnHm/0Vx5v9Ic+j/THjt/1R/9P9ahvv/yM7h/+fi2P/Fytz/WIP3/1eC9v8hXPv/AEX//wBF//8ARf//AEX//wBF/10ARf+eAEX//wBF//8ARf//AEX//zRp+v9Yg/j/WYT6/1mF+//x7un/7+vk/+Tf1P9Le/3/W4f+/1uH/v9ch/7/XIf+/1uH/v9bh/7/TX3+/x5e/v/x7un/7+vk/+Tf1P9Zhfr/WIT5/zZr+v8ARf//AEX//wBF//8ARf//AEX/nQBF/8wARf//AEX//wBF//8XVPr/T3z2/1mF+v9ahvv/W4b8//Xy7v/y8Or/5d/U/xZV/v9ciP//XIj//1yI//9ciP//XIj//1yI//8YVv3/AE7///Xy7v/y8Or/5d/U/1qG/P9Zhfv/T3z3/xZU+f8ARf//AEX//wBF//8ARf/QAET97wBF//8ARf//EVD7/1B77/9hifT/nbHp/6G06/+lufL/+Pbz//f18f/r597/eJXn/56w5f+esOX/g6Dw/1yI//9mjvz/nrLr/3yY6v94mvL/+Pbz//f18f/r597/n7Lo/52w5P+breH/eZbk/xBQ+/8ARf//AEX//wBE/u0ARf/9AEX//wBF//84bPj/WIP4/6O49P/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j39f/XIj//6S69v/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j3tf/OW34/wBF//8ARf//AEX//gBF//4ARf//AEX//0d4+/9bh/3/qL/8//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9ciP//qL/9//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9IePv/AEX//wBF//8ARf/9AEX97QBF//8ARf//PXH9/1yI/v9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/1yI//9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/z5y/f8ARf//AEX//wBF/u4ARf/QAEX//wBF//8WVf7/W4f//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9bh///FlX+/wBF//8ARf//AET/zQBG/50ARf//AEX//wBF//8iXv//VoT//1yI//9ciP//U4H///39/P/18+7/5eDV/1yI//9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj///39/P/18+7/5eDV/1yI//9ciP//V4T//yFd//8ARf//AEX//wBF//8ARf+eAET/XQBF//8ARf//AEX//wBF//8ESP//FlX//xRT//8CR///9/n8//b08P/j39r/UoH//1yI//9ciP//XIj//1yI//9ciP//XIj//1KB//82bP//9/n8//b08P/i39r/FVT//xZV//8ESP//AEX//wBF//8ARf//AEX//wBF/1wARf8QAEX/9wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//9LfP7/fZ76/0d29/8ARf//Ckz//xdW//8UR87/G03T/xdW//8KTP//AEX//wBF//9LfP7/fZ76/0d29/8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/4ACL/EAAAAAAARv+gAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wU+2f8OPr3/AEX//wBF//8ARf//D0/7/yxi9v8nXvb/B0n9/wBF//8ARf//AEX//wBF//8ARf//AEX//wBH/58AAAAAAAAAAABG/ywARf/5AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT3/wo3rv8MRNv/AEX//xZT+v9RfPD/VX/y/1R/8v9JdvH/B0n9/wBF//8ARf//AEX//wBF//8ARf/7AEf/JQAAAAAAAAAAAAAAAABF/5EARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//A0Hn/wo3rf8OPr//JlTQ/1J+8/9ahvz/Wob8/1mF+f8lX/v/AEX//wBF//8ARf//AEX//wBT/5AAAAAAAAAAAAAAAAAAAAAAAEj/CABF/9gARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT4/wU+1/8dS8j/Un70/1yI//9ciP7/XIj+/ypj/f8ARf//AEX//wBF//8ARf/XAEj/CgAAAAAAAAAAAAAAAAAAAAAAAAAAAKL/IgBF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//yJe/v9ciP//XIj//12J//9fiv//DE7//wBF//8ARf//AEX/6wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD3/NABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//x9c//8/c///PnL//xFS//8ARf//AEX//wBF/+sAPf80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGL/JABF/9cARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/YACP/IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEP/CgBT/48ARf/7AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/5AEX/kQBI/wgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABI/yUAR/+gAEX/+ABF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/3AEX/oABG/ywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo/8QAEX/XABF/54ARf/MAET/7gBF//0ARf/+AEX/7QBF/88ARv+dAEX/XQBF/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAD/4AAAP/AAAA/4AAAH/AAAA/gAAAHwAAAA8AAAAOAAAABgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH8AAAD/gAAB/8AAA//wAA///AA/8=
// @run-at document-end
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==


(function() {
    'use strict';


    const REVISION = 3;

    const pp_app = document.body.querySelector(`shreddit-app`);
    if(pp_app == null || pp_app.getAttribute(`devicetype`) != `desktop`)
    {
        console.log(`Reddit++ was stopped for a non compatible page`);
        return;
    }

    let pp_meta = document.head.querySelector(`meta[name="reddit-plus-plus"]`);
    if(pp_meta != null)
    {
        console.log(`Reddit++ already runned`);
        return;
    }

    pp_meta = document.createElement(`meta`);
    pp_meta.setAttribute(`name`, `reddit-plus-plus`);
    pp_meta.setAttribute(`rev`, REVISION);
    document.head.append(pp_meta);



    // ***********************************************************************************************************************
    // ************************************************** CLASSES ************************************************************
    // ***********************************************************************************************************************

    class Database
    {
        constructor(name, isCleanupable) {
            this.databaseKey = name+`_DATABASE`;
            this.refreshKey = name+`_REFRESHED`;
            this.cleanupKey = name+`_CLEANUP`;
            this.isCleanupable = (isCleanupable != undefined && isCleanupable == true);
            this.refresh();

            // cleanup database
            if(this.isCleanupable && GM_getValue(this.cleanupKey, 0) < Date.now())
            {
                // limit data storage for 1 day
                const timestampLimit = Date.now() - (1000 * 60 * 60 * 24);

                this.data = Object.fromEntries(Object.entries(this.data).filter(([key, value]) => value.timestamp > timestampLimit) );
                this.refreshed = Date.now();

                GM_setValue(this.databaseKey, this.data);
                GM_setValue(this.refreshKey, this.refreshed);
                GM_setValue(this.cleanupKey, Date.now() + (1000 * 60 * 10));
            }
        }

        refresh()
        {
            const lastRefreshed = GM_getValue(this.refreshKey, 0);
            if(this.data == undefined || this.refreshed < lastRefreshed)
            {
                this.refreshed = lastRefreshed;
                this.data = GM_getValue(this.databaseKey, {});
            }
        }

        get(id)
        {
            this.refresh();

            const data = this.data[id];

            return (data == undefined) ? {} : data;
        }

        isDisabled(id)
        {
            return this.get(id) == false;
        }

        isEnabled(id)
        {
            return !this.isDisabled(id);
        }

        set(id, value)
        {
            this.refresh();

            if(this.isCleanupable)
            {
                value.timestamp = Date.now();
            }

            this.data[id] = value;
            this.refreshed = Date.now();

            GM_setValue(this.databaseKey, this.data);
            GM_setValue(this.refreshKey, this.refreshed);
        }
    }

    class CustomCSS
    {
        constructor()
        {
            this.stylesheet = new CSSStyleSheet();
            this.registry(document);
        }

        registry(source)
        {
            source.adoptedStyleSheets.push(this.stylesheet);
        }

        addRule(rule)
        {
            this.stylesheet.insertRule(rule, 0);
        }

        addVar(name, lightValue, darkValue)
        {
            this.addRule(`:root.theme-light { ${name}: ${lightValue} !important;}`);
            this.addRule(`:root { ${name}: ${darkValue ?? lightValue};}`);
        }
    }

    class Window
    {
        constructor(tittle, render, onClose) {
            this.tittle = tittle;
            this.render = render;
            this.onClose = onClose;
            this.container = null;
            this.content = null;
            this.closeButton = null;
        }

        build()
        {
            this.container = document.createElement(`div`);
            this.container.classList.add(`pp_WindowContainer`);
            this.container.addEventListener('click', e => { if(e.target == this.container) { this.close(); }});

            const win = AppendNew(this.container, `div`, `pp_Window`);

            const tittleContainer = AppendNew(win, `div`, `pp_WindowTittleContainer`);

            let tittle = AppendNew(tittleContainer, `div`, [`pp_WindowTittle`, `flex`, `flex-row`]);
            tittle = AppendNew(tittle, `span`, [`text-24`, `font-semibold`]);
            tittle.textContent = this.tittle;


            this.closeButton = AppendNew(tittleContainer, `div`, [`pp_WindowCloseButton`, `flex`, `items-center`]);
            this.closeButton = AppendNew(this.closeButton, `button`, [`button`, `icon`, `inline-flex`, `items-center`, `justify-center`, `button-small`, `button-secondary`, `px-[var(--rem6)]`]);
            this.closeButton.setAttribute(`tittle`, `Close ${this.tittle}`);
            this.closeButton.addEventListener('click', e => { this.close(); });

            this.closeButton = AppendNew(this.closeButton, `span`, [`flex`, `items-center`, `justify-center`]);
            this.closeButton = AppendNew(this.closeButton, `span`, [`flex`]);

            const svg = buildSvg(20, 20, closeWindowGraphic, {c:'none'});
            svg.setAttribute(`height`, `16`);
            svg.setAttribute(`width`, `16`);
            this.closeButton.append(svg);

            AppendNew(win, `hr`, `border-b-neutral-border-weak`); //`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`

            this.content = AppendNew(win, `div`, `pp_WindowContent`);

            AppendNew(win, `div`, `pp_WindowFooter`).textContent = ` `;
        }

        open(context)
        {
            if(this.container == null)
            {
                this.build();
            }

            while (this.content.firstChild) {
                this.content.removeChild(this.content.lastChild);
            }

            this.render(this, context);

            document.body.appendChild(this.container);
            document.body.style.overflow = 'hidden';
        }

        close()
        {
            this.container.remove();

            document.body.style.overflow = 'visible';

            if(this.onClose != undefined)
            {
                this.onClose();
            }
        }
    }

    class ImageViewer
    {
        constructor()
        {
            this.openned = false;
            this.viewer = null;
            this.container = null;
            this.image = null;

            this.mouse = {x:0, y:0};
            this.drag = { enabled:false, start: {x:0, y:0}, current: {x:0, y:0, scale:1} };

            this.scrollImage = this.scrollImage.bind(this);
            this.startDrag = this.startDrag.bind(this);
            this.mouseMove = this.mouseMove.bind(this);
            this.endDrag = this.endDrag.bind(this);
        }

        open(src)
        {
            if(this.openned) return;
            this.openned = true;

            if(this.viewer == null)
            {
                this.build();
            }

            this.image.src = src;


            window.addEventListener('wheel', this.scrollImage, { passive: false });
            this.image.addEventListener('mousedown', this.startDrag);
            document.addEventListener('mousemove', this.mouseMove );
            this.image.addEventListener('mouseup', this.endDrag);
            this.image.addEventListener('mouseleave', this.endDrag);

            // reset pos
            this.drag.current = {x:0, y:0, scale:1};

            this.updateTransform();

            document.body.appendChild(this.viewer);
        }

        close()
        {
            this.viewer.remove();

            this.drag.enabled = false;

            this.container.classList.toggle(`pp_imageViewer_drag`, false);

            window.removeEventListener('wheel', this.scrollImage, { passive: false });
            this.image.removeEventListener('mousedown', this.startDrag);
            document.removeEventListener('mousemove', this.mouseMove);
            this.image.removeEventListener('mouseup', this.endDrag);
            this.image.removeEventListener('mouseleave', this.endDrag);

            this.openned = false;
        }

        build()
        {
            this.viewer = document.createElement(`div`);
            this.viewer.classList.add(`pp_imageViewer`);
            this.viewer.dataset.open = false;

            const closeButton = AppendNew(this.viewer, `div`, `pp_imageViewer_closeButton`);

            const closeSvg = buildSvg(40, 40, closeImageGraphic, {w:1});
            closeButton.appendChild(closeSvg);

            this.container = AppendNew(this.viewer, `div`, `pp_imageViewer_imageContainer`);

            this.image = AppendNew(this.container, `img`, `pp_imageViewer_image`);
            this.image.alt = `Comment image`;
            this.image.ondragstart = function() { return false; };


            // close
            this.viewer.addEventListener('click', (e) => {
                if(e.target != this.image)
                {
                    this.close();
                }
            });

            closeButton.addEventListener('click', () => { this.close(); });
        }

        updateTransform()
        {
            this.container.style.transform = `translate(${this.drag.current.x}px, ${this.drag.current.y}px) scale(${this.drag.current.scale}, ${this.drag.current.scale})`;
        }

        startDrag(event)
        {
            this.drag.start.x = event.screenX - this.drag.current.x;
            this.drag.start.y = event.screenY - this.drag.current.y;
            this.drag.enabled = true;

            this.container.classList.toggle(`pp_imageViewer_drag`, true);
        }

        mouseMove(event)
        {
            this.mouse.x = event.clientX;
            this.mouse.y = event.clientY;

            if(this.drag.enabled)
            {
                this.drag.current.x = event.screenX - this.drag.start.x;
                this.drag.current.y = event.screenY - this.drag.start.y;

                this.updateTransform();
            }
        }

        endDrag()
        {
            this.fit(1);

            this.drag.enabled = false;

            this.container.classList.toggle(`pp_imageViewer_drag`, false);
        }

        scrollImage(e)
        {
            const m = Math.max(1.0, 1.0 + Math.log2(this.drag.current.scale * this.drag.current.scale));

            const prevScale = this.drag.current.scale;

            this.drag.current.scale = Math.max(0.5, this.drag.current.scale + (-e.deltaY / 1000) * m );

            console.log(`scale mouse [${this.mouse.x}, ${this.mouse.y}]`);

            const rect = this.image.getBoundingClientRect();

            const hh = rect.height / 2;
            const hw = rect.width / 2;

            const dy = rect.y + hh;
            const dx = rect.x + hw;

            const os = ( this.drag.current.scale / prevScale - 1);
            this.drag.current.y -= Math.min(Math.max(this.mouse.y - dy, -hh), hh) * os;
            this.drag.current.x -= Math.min(Math.max(this.mouse.x - dx, -hw), hw) * os;


            if(e.deltaY > 0)
            {
                this.drag.current.y /= 1.1;
                this.drag.current.x /= 1.1;
            }


            this.fit(0.33);

            e.preventDefault();
        }

        fit(force)
        {
            const offset = 0;
            const rect = this.image.getBoundingClientRect();

            const left = offset - rect.left;
            const right = rect.right - window.innerWidth + offset;

            if(left > 0 && right < 0)
            {
                this.drag.current.x += ((rect.width > window.innerWidth) ? -right : left) * force;
            }
            else if(left < 0 && right > 0)
            {
                this.drag.current.x += ((rect.width > window.innerWidth) ? left : -right) * force;
            }

            const top = offset - rect.top;
            const bottom = rect.bottom - window.innerHeight + offset;

            if(top > 0 && bottom < 0)
            {
                this.drag.current.y += ((rect.height > window.innerHeight) ? -bottom : top) * force;
            }
            else if(top < 0 && bottom > 0)
            {
                this.drag.current.y += ((rect.height > window.innerHeight) ? top : -bottom) * force;
            }

            this.updateTransform();
        }

    }





    // ***********************************************************************************************************************
    // **************************************************** VARS *************************************************************
    // ***********************************************************************************************************************

    const HOUR_SECONDS = 60 * 60;
    const DAY_SECONDS = HOUR_SECONDS * 24;


    const settingsButtonGraphic = 'M15.07,2.25a.33.33,0,0,1,.33.33V6.72h1.25a2.11,2.11,0,0,1,0,4.21H15.4v4.14a.33.33,0,0,1-.33.33H10.93v1.25a2.11,2.11,0,0,1-4.21,0V15.4H2.58a.33.33,0,0,1-.33-.33v-3A3.51,3.51,0,0,0,4.49,8.82,3.48,3.48,0,0,0,2.25,5.57v-3a.33.33,0,0,1,.33-.33H5.34a3.49,3.49,0,0,0,7,0h2.76m0-1.25H10.75A2.24,2.24,0,1,1,6.9,1H2.58A1.58,1.58,0,0,0,1,2.58v4a2.24,2.24,0,1,1,0,4.47v4a1.58,1.58,0,0,0,1.58,1.58H5.47a3.36,3.36,0,0,0,6.71,0h2.89a1.58,1.58,0,0,0,1.58-1.58V12.18a3.36,3.36,0,0,0,0-6.71V2.58A1.58,1.58,0,0,0,15.07,1Z';
    const settingsGraphic = [{d:'M10 20c-.401 0-.802-.027-1.2-.079a1.145 1.145 0 0 1-.992-1.137v-1.073a.97.97 0 0 0-.627-.878A.98.98 0 0 0 6.1 17l-.755.753a1.149 1.149 0 0 1-1.521.1 10.16 10.16 0 0 1-1.671-1.671 1.149 1.149 0 0 1 .1-1.523L3 13.906a.97.97 0 0 0 .176-1.069.98.98 0 0 0-.887-.649H1.216A1.145 1.145 0 0 1 .079 11.2a9.1 9.1 0 0 1 0-2.393 1.145 1.145 0 0 1 1.137-.992h1.073a.97.97 0 0 0 .878-.627A.979.979 0 0 0 3 6.1l-.754-.754a1.15 1.15 0 0 1-.1-1.522 10.16 10.16 0 0 1 1.673-1.676 1.155 1.155 0 0 1 1.522.1L6.1 3a.966.966 0 0 0 1.068.176.98.98 0 0 0 .649-.887V1.216A1.145 1.145 0 0 1 8.8.079a9.129 9.129 0 0 1 2.393 0 1.144 1.144 0 0 1 .991 1.137v1.073a.972.972 0 0 0 .628.878A.977.977 0 0 0 13.905 3l.754-.754a1.152 1.152 0 0 1 1.522-.1c.62.49 1.18 1.05 1.671 1.671a1.15 1.15 0 0 1-.1 1.522L17 6.1a.967.967 0 0 0-.176 1.068.98.98 0 0 0 .887.649h1.073a1.145 1.145 0 0 1 1.137.991 9.096 9.096 0 0 1 0 2.392 1.145 1.145 0 0 1-1.137.992h-1.073A1.041 1.041 0 0 0 17 13.905l.753.755a1.149 1.149 0 0 1 .1 1.521c-.49.62-1.05 1.18-1.671 1.671a1.149 1.149 0 0 1-1.522-.1L13.906 17a.97.97 0 0 0-1.069-.176.981.981 0 0 0-.65.887v1.073a1.144 1.144 0 0 1-.99 1.137A9.431 9.431 0 0 1 10 20Zm-.938-1.307a7.638 7.638 0 0 0 1.875 0v-.982a2.292 2.292 0 0 1 3.853-1.6l.693.694a8.796 8.796 0 0 0 1.326-1.326l-.694-.694a2.29 2.29 0 0 1 1.6-3.851h.982a7.746 7.746 0 0 0 0-1.876h-.982a2.213 2.213 0 0 1-2.034-1.4 2.223 2.223 0 0 1 .438-2.451l.694-.693a8.76 8.76 0 0 0-1.327-1.326l-.692.694a2.22 2.22 0 0 1-2.434.445 2.221 2.221 0 0 1-1.419-2.041v-.979a7.638 7.638 0 0 0-1.875 0v.982a2.213 2.213 0 0 1-1.4 2.034 2.23 2.23 0 0 1-2.456-.438l-.693-.694a8.757 8.757 0 0 0-1.326 1.327l.694.692a2.216 2.216 0 0 1 .445 2.434 2.22 2.22 0 0 1-2.041 1.418h-.982a7.746 7.746 0 0 0 0 1.876h.982a2.213 2.213 0 0 1 2.034 1.4 2.223 2.223 0 0 1-.438 2.451l-.694.693c.394.488.838.933 1.326 1.326l.694-.694a2.218 2.218 0 0 1 2.433-.445 2.22 2.22 0 0 1 1.418 2.041v.983ZM10 13.229a3.23 3.23 0 1 1 0-6.458 3.23 3.23 0 0 1 0 6.458Zm0-5.208a1.979 1.979 0 1 0 0 3.958 1.979 1.979 0 0 0 0-3.958Z'}];
    const closeWindowGraphic = [{d:'m18.442 2.442-.884-.884L10 9.116 2.442 1.558l-.884.884L9.116 10l-7.558 7.558.884.884L10 10.884l7.558 7.558.884-.884L10.884 10l7.558-7.558Z'}];
    const linkGraphics = [{d:'M14.111 12.5a3.701 3.701 0 0 1-1.09 2.41c-.479.47-.928.922-1.378 1.373-.45.45-.894.9-1.368 1.366a3.852 3.852 0 0 1-2.698 1.099 3.852 3.852 0 0 1-2.698-1.099 3.738 3.738 0 0 1-1.116-2.659c0-.997.402-1.953 1.116-2.658.479-.472.928-.923 1.378-1.375.45-.45.893-.9 1.368-1.365A3.936 3.936 0 0 1 9.638 8.59a3.968 3.968 0 0 1 2.24.258c.27-.269.546-.54.812-.806l.131-.13a5.086 5.086 0 0 0-3.182-.624A5.052 5.052 0 0 0 6.732 8.71c-.48.471-.929.922-1.377 1.373-.449.451-.894.9-1.37 1.366A4.982 4.982 0 0 0 2.5 14.992c0 1.328.534 2.602 1.486 3.543A5.13 5.13 0 0 0 7.58 20a5.13 5.13 0 0 0 3.595-1.465c.478-.471.927-.923 1.377-1.374.451-.451.894-.9 1.368-1.366a4.993 4.993 0 0 0 1.263-2.071c.243-.781.288-1.61.132-2.412L14.11 12.5Z'}, {d:'M16.017 1.467A5.123 5.123 0 0 0 12.422 0a5.123 5.123 0 0 0-3.595 1.467c-.478.471-.926.923-1.377 1.374-.45.451-.894.9-1.367 1.366a4.966 4.966 0 0 0-1.106 1.624 4.907 4.907 0 0 0-.291 2.86l1.2-1.19a3.699 3.699 0 0 1 1.092-2.41c.478-.472.928-.923 1.377-1.374.45-.45.894-.9 1.368-1.366a3.844 3.844 0 0 1 2.698-1.101c1.012 0 1.982.396 2.698 1.101a3.736 3.736 0 0 1 1.116 2.66c0 .996-.401 1.953-1.116 2.658-.478.471-.927.922-1.377 1.373-.45.451-.893.9-1.368 1.367a3.933 3.933 0 0 1-2.014 1.003 3.966 3.966 0 0 1-2.24-.26c-.273.274-.551.549-.818.818l-.123.12a5.087 5.087 0 0 0 3.183.624 5.053 5.053 0 0 0 2.906-1.423c.477-.472.926-.923 1.376-1.375.45-.452.894-.9 1.368-1.365A4.977 4.977 0 0 0 17.5 5.008a4.977 4.977 0 0 0-1.488-3.543l.005.002Z'}];
    const newUserGraphic = [{w:'2', f:'none', d:'M12.5,11.5a3.39,3.39,0,0,1,2,2,3.16,3.16,0,0,1,0,2'}, {w:'0', d:'M1.46,1.5S3.49,4.89,7,5.07c1.49.07,3.35.25,4.06.79,1.41,1.09,2.3,2.08,1.74,4.37a4.91,4.91,0,0,1-4.36,3.49C5.08,14,2.89,10.29,2.33,9.35.41,6.12,1.46,1.5,1.46,1.5Z'}];
    const closeImageGraphic = [{d:'m33.16001,9.52439l-2.37671,-2.37671l-10.691,10.691l-10.691,-10.691l-2.37671,2.37671l10.691,10.691l-10.691,10.691l2.37671,2.37671l10.691,-10.691l10.691,10.691l2.37671,-2.37671l-10.691,-10.691l10.691,-10.691z'}];

    const feedGraphics =
          {
              Best:[{c:'none', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
              Hot:[{w:'0.5', d:'M8.49,2.93c.7,1.56,3,2.81,3.69,3.52a5.14,5.14,0,0,1,1.36,5.45c-1.09,3.37-4.49,3.38-6.21,3.38s-4.18-.28-5-3,.8-4.41,1-5,1.06,2.52,2,3.12c1.19.79,2.85,0,2.85-1.18S6.72,7.65,6.44,5.37a10.59,10.59,0,0,1,1-4.9S7.83,1.46,8.49,2.93Z'}],
              New:[{f:'none', w:'1.5', d:'M7.5,3 L7.5,9 L12,6'}, {w:'0.5', d:'M8,15.5A7.5,7.5,0,1,1,15.5,8,7.5,7.5,0,0,1,8,15.5Zm0-14A6.5,6.5,0,1,0,14.5,8,6.51,6.51,0,0,0,8,1.5Z'}],
              Top:[{f:'none', w:'1.5', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
              Rising:[{f:'none', w:'2', d:'M8.5,4.5l4.68-2.45S14,5.31,14.5,7.5'}, {f:'none', w:'2',d:'M1.1,14.67c1.4-2.8,3.62-6.84,3.62-6.84L9,12.18l4.13-9.94'}]
          };

    const tagGraphics =
          {
              Followed : [{d:'m0.43678,5.49532l4.46205,-0.8817l2.13151,-3.90349l2.39985,3.78849l4.1937,0.9967l-3.65339,3.14905l1.13489,4.59352l-4.19007,-2.03743l-3.96007,2.03743l0.86655,-4.78519l-3.38505,-2.95738l0,-0.00001l0.00001,0z'}],
              Liked : [{d:'m5.10986,11.44595c-3.39081,-2.55084 -4.60316,-4.16445 -4.61299,-6.1398c-0.00896,-1.80077 1.48763,-3.53879 3.03857,-3.52877c0.77447,0.00501 2.43577,0.6678 3.02404,1.20648c0.29641,0.27142 0.4368,0.24465 1.09671,-0.20908c1.79601,-1.23486 3.54983,-1.26078 4.68568,-0.06924c1.81534,1.90433 1.48483,4.17844 -0.95008,6.53732c-1.29454,1.25411 -4.12153,3.47898 -4.4205,3.47898c-0.09098,0 -0.92862,-0.57415 -1.86143,-1.27588l0,0l0,-0.00001z'}],
              Warning : [{d:'m0.74313,12.0159l6.28126,-9.83543l6.28126,9.83543l-12.56252,0z'}, {d:'m6.91754,5.38201l0,4.06927'}, {d:'m6.91754,10.29973l0,0.7153'}],
              Blocked : [{d:'M3.11,3l7.94,8'}, {d:'M7,12.88A5.88,5.88,0,1,1,12.91,7,5.88,5.88,0,0,1,7,12.88Z'}]
          };

    const tagButtonGraphics =
          {
              Followed: [{d:'m1.56177,7.99265l5.62517,-1.11153l2.68713,-4.92101l3.02542,4.77603l5.28687,1.25651l-4.60571,3.96991l1.43072,5.79091l-5.28229,-2.56852l-4.99233,2.56852l1.09243,-6.03254l-4.26742,-3.72827l0,-0.00001z'}],
              Liked: [{d:'m7.28935,15.93702c-4.80908,-3.55432 -6.52852,-5.80272 -6.54246,-8.55516c-0.0127,-2.50919 2.10985,-4.93093 4.30951,-4.91697c1.09841,0.00698 3.45457,0.93051 4.2889,1.6811c0.42039,0.37819 0.6195,0.3409 1.55543,-0.29133c2.54723,-1.72064 5.03461,-1.75676 6.64556,-0.09648c2.57463,2.65349 2.10589,5.82222 -1.34747,9.10906c-1.836,1.74747 -5.84543,4.84759 -6.26946,4.84759c-0.12903,0 -1.31703,-0.80001 -2.64001,-1.7778l0,0l0,-0.00001z'}],
              Warning: [{d:'m1.24313,16.49297l8.78125,-13.75l8.78125,13.75l-17.5625,0z'}, {d:'m9.875,7.5l0,4.5'}, {d:'m9.875,14.09375l0,1'}],
              Blocked: [{d:'m9.78126,18.09375c-4.41989,0 -8,-3.58011 -8,-8c0,-4.41989 3.58011,-8 8,-8c4.41989,0 8,3.58011 8,8c0,4.41989 -3.58011,8 -8,8z'}, {d:'m4.43767,4.59392l10.81217,10.93716'}]
          };

    const USERTAG_FOLLOWED = `Followed`;
    const USERTAG_LIKED = `Liked`;
    const USERTAG_WARNING = `Warning`;
    const USERTAG_BLOCKED = `Blocked`;

    const TAG_CONFIG_PRIORITY = 0;
    const TAG_CONFIG_HINT = 1;
    const TAG_CONFIG_HINT_ACTIVATED = 2;
    const TAG_CONFIG_COLOR = 3;

    const tagConfigs =
          {
              Followed:[100, `Follow`, `Unfollow`, `#0b7ed3`],
              Liked:[2, `Tag as liked`, `Remove liked tag`, `#C95A54`],
              Warning:[1, `Tag as warned`, `Remove warned tag`, `#D4A343`],
              Blocked:[0, `Block`, `Unblock`, `#663988`]

          };

    const FEED_BUTTONS_EXTENDED = [`Best`, `Hot`, `New`, `Top`, `Rising`];
    const FEED_BUTTONS = [`Hot`, `New`, `Top`, `Rising`];


    const SETTING_WIDE_MODE = `wideMode`;

    const SETTING_FEED_BUTTONS = `feedButtons`;
    const SETTING_FLAIR_BAR = `flairbar`;
    const SETTING_BACKPLATES = `backplates`;

    const SETTING_USER_INFO = `userInfo`;
    const SETTING_USER_TAGS = `userTags`;
    const SETTING_SHOW_NAMES = `showNames`;
    const SETTING_BIGGER_FONTS = `biggerFonts`;
    const SETTING_HIDE_SHARE = `hideShare`;
    const SETTING_GHOSTED_COMMENTS = `ghostedComments`;


    const css = new CustomCSS();

    let settingsRevision = getSettingsRevision();

    let settings = new Database(`SETTINGS`);
    let tags = new Database(`TAGS`);
    let users = new Database(`USERS`, true);
    let flairs = new Database(`FLAIRS`);
    let subs = new Database(`SUBS`, true);

    let flairsWindow = new Window(`Subreddit flairs settings`, renderFlairsWindow);
    let settingsWindow = new Window(`Reddit++ settings`, renderSettingsWindow);

    let imageViewer = new ImageViewer();



    // ***********************************************************************************************************************
    // ***************************************** backward compatibility ******************************************************
    // ***********************************************************************************************************************

    let legacy_subs = GM_getValue(`SUB_DATABASE`, null);

    if(legacy_subs != null)
    {
        for (const [key, value] of Object.entries(legacy_subs)) {
            if(value.flairs == undefined) continue;

            for(const flair of value.flairs)
            {
                if(flair.banned != undefined && flair.banned == true)
                {
                    const flairData = flairs.get(key);
                    if(flairData.banned == undefined)
                    {
                        flairData.banned = [];
                    }
                    flairData.banned.push(flair.text);
                    flairs.set(key, flairData);
                }

                if(flair.hidden != undefined && flair.hidden == true)
                {
                    const flairData = flairs.get(key);
                    if(flairData.hidden == undefined)
                    {
                        flairData.hidden = [];
                    }
                    flairData.hidden.push(flair.text);
                    flairs.set(key, flairData);
                }
            }
        }

        GM_setValue(`SUB_DATABASE`, null);
    }



    // ***********************************************************************************************************************
    // ************************************************ COMMON CSS ***********************************************************
    // ***********************************************************************************************************************

    css.addVar('--color-neutral-background-transparent', '#fff0', '#0b141600');
    css.addVar('--stickiedColor', '#0e8a001c', '#0e8a001c');
    css.addVar('--stickiedHoverColor', '#18900b3d', '#18900b3d');


    // windows
    css.addRule('.pp_WindowContainer { cursor:pointer; position: fixed; top:0px;  z-index: 10; width:100%; height:100%; display: flex; justify-content: center; align-items: center; background-color: #000000b3; }');
    css.addRule('.pp_Window { cursor: auto; display: flex; flex-direction: column; width:900px; height: fit-content; min-height: 200px; max-height: 75%; border-radius: 15px; background-color: var(--color-neutral-background); box-shadow: 0px 0px 50px 0px #00000070; }');
    css.addRule('.pp_WindowTittleContainer { height: 48px; margin: 1rem; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }');
    css.addRule('.pp_WindowTittle { margin-left: 1rem; }');
    css.addRule('.pp_WindowCloseButton { margin: 1rem; }');
    css.addRule('.pp_WindowContent { display: flex; flex-direction: column; overflow-y: overlay; }');
    css.addRule('.pp_WindowFooter { height: 2rem; min-height: 2rem; }');

    css.addRule('.pp_WindowElementsContainer { display: flex; flex-direction: column; padding: 0px; margin: 20px 40px; gap: 0.5rem;}');
    css.addRule('.pp_WindowElement { display: flex; flex-direction: row; justify-content: flex-end; height: 3rem; }');
    css.addRule('.pp_WindowElementsContainer > .pp_WindowElement:hover { background-color: var(--color-neutral-background-hover); border-radius: 15px;}');

    // image viewer
    css.addRule('.pp_imageViewer { display: flex; justify-content: center; align-items: center; position: fixed; top:0px;  z-index: 10; cursor:pointer;  width:100%; height:100%; background-color: #000000b3; }');
    css.addRule('.pp_imageViewer_closeButton { display: flex; align-items: center; justify-content: center; position: fixed; top:50px; z-index: 11; right:50px; cursor:pointer; width:50px; height:50px; color: #ffffff9c; background-color: #00000069; border-radius:30px; }');
    css.addRule('.pp_imageViewer_closeButton:hover { color: #ffffffc7; }');
    css.addRule('.pp_imageViewer_imageContainer {display: flex; justify-content: center; align-items: center; width:80%; height:90%;}');
    css.addRule('.pp_imageViewer_imageContainer:not(.pp_imageViewer_drag) { transition: transform 0.5s;}');
    css.addRule('.pp_imageViewer_image {cursor: grab; object-fit: scale-down; max-width:100%; max-height:100%; box-shadow: 0px 0px 20px 3px #14141485; }');
    css.addRule('.pp_imageViewer_image:active {cursor: grabbing; }');

    // ban hint
    css.addRule('faceplate-banner { max-width: 1000px !important;}');


    if(settings.isEnabled(SETTING_BIGGER_FONTS))
    {
        // resize fonts
        css.addRule('.text-14 { font-size: 1.0rem !important; line-height: 1.4rem !important;}');
        css.addRule('faceplate-hovercard .text-12 { font-size: 0.9rem !important; font-color: #595959 !important;}');

        // comments more space
        css.addRule('shreddit-comment-action-row { margin-bottom: 15px !important;}');
    }



    // hide native share
    if(settings.isEnabled(SETTING_HIDE_SHARE))
    {
        css.addRule('shreddit-comment-share-button { display:none !important;}');
    }


    // posts gradient selection
    if(settings.isEnabled(SETTING_BACKPLATES))
    {
        //[view-context="SubredditFeed"]
        css.addRule('article > shreddit-post { background-color: #00000000 !important; padding-top:10px !important; margin-top: 10px !important; margin-bottom: 10px !important;}'); //border-radius: 0px !important;

        css.addRule('article > shreddit-post::before { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 0; z-index: -1;  background: linear-gradient(var(--color-neutral-background-hover), var(--color-neutral-background)); transition: opacity 0.2s;}');
        css.addRule('article > shreddit-post:hover::before { opacity: 1;}');

        // override gold
        css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""]) { background-image: linear-gradient(rgba(255, 214, 53, 0.2), rgba(255, 214, 53, 0)) !important;}');
        css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""])::before { background: linear-gradient(#fbed2966, var(--color-neutral-background)) !important;}');

        // stickied
        css.addRule('.stickied::after { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 1; z-index: -2;  background: linear-gradient(var(--stickiedColor), var(--color-neutral-background)) !important;}');
        css.addRule('.stickied::before { background: linear-gradient(var(--stickiedHoverColor), var(--color-neutral-background)) !important;}');

    }





    // ***********************************************************************************************************************
    // ************************************************ Events ***************************************************************
    // ***********************************************************************************************************************

    // Settings changes
    window.addEventListener("storage", (event) => {
        const currentRevision = getSettingsRevision();

        if(currentRevision != settingsRevision)
        {
            settingsRevision = currentRevision;
            document.addEventListener("visibilitychange", () => { window.location.reload(); }, {once:true});
        }
    });


    // Intersections
    const commentRenderIntersector = new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting)
            {
                renderComment(entry.target.parentNode);

                commentRenderIntersector.unobserve(entry.target);
            }
        }

    }, { threshold: 0.05});


    // Mutations
    new MutationObserver(mutations => {
        const hasComments = window.location.href.includes(`/comments/`);

        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {

                if (node instanceof HTMLElement)
                {
                    // check new posts
                    if(node.matches(`faceplate-batch`))
                    {
                        node.querySelectorAll(`shreddit-post`).forEach((post) => {
                            renderPost(post);
                        });
                    }

                    // check new comments
                    if(hasComments)
                    {
                        // post tree
                        if(node.id != null && node.id.includes(`comment-tree`) == true)
                        {
                            registryAllComments(node);
                        }

                        // single thread
                        if(node.matches(`shreddit-comment-tree`) == true)
                        {
                            registryAllComments(node);
                        }

                        // more comments
                        if(node.matches(`shreddit-comment`) == true)
                        {
                            registryComment(node);

                            registryAllComments(node);
                        }
                    }


                    // check refresh header
                    if(node.matches(`reddit-header-large`) == true)
                    {
                        renderHeader(node.parentNode);
                    }

                    // check refresh sub pages
                    if(node.matches(`dsa-transparency-modal-provider`) == true)
                    {
                        renderApp(node);

                        renderFeedButtons(node);

                        renderSub(node);

                        checkNativePosts(node);
                    }

                    // check refresh main pages
                    if(node.classList.contains(`grid-container`) && node.querySelector(`.main-container`) != null)
                    {
                        renderApp(node);

                        renderFeedButtons(node);

                        checkNativePosts(node);
                    }
                }
            }
        }
    }).observe(document.body, {childList: true, subtree: true});








    // ***********************************************************************************************************************
    // ************************************************ CORE APP *************************************************************
    // ***********************************************************************************************************************

    if(settings.isEnabled(SETTING_WIDE_MODE))
    {
        css.addRule('@media (min-width: 1392px) { .pp_pageContainer { margin-right: 300px;}}');
        css.addRule('@media (min-width: 1392px) { .pp_mainFeed { width: 700px !important;  }}');
        css.addRule('@media (min-width: 1392px) { .pp_rightSidebar { grid-column-start: 3;  order: 10;}}');

        css.addRule('@media (min-width: 1392px) { #right-sidebar-container { position: fixed; right: 0px;  margin: 15px 10px 0px 0px; } }');

        css.addRule('@media (min-width: 1392px) { #left-sidebar-container { position: fixed; left: 0px; max-width: 250px; transition: 0.2s}}');
        css.addRule('@media (min-width: 1392px) { #left-sidebar-container:hover { max-width: 300px;}}');
        css.addRule('@media (min-width: 1392px) { #left-sidebar > nav { flex-wrap: wrap;}}');
    }


    renderApp(document.body);

    function renderApp(container)
    {
        if(settings.isDisabled(SETTING_WIDE_MODE)) return;


        const leftSidebar = container.querySelector(`#left-sidebar-container`);

        // Firefox first-load fix
        if(awaitElement(leftSidebar, renderApp)) return;


        const pageContainer = leftSidebar.parentNode;
        pageContainer.classList.add(`pp_pageContainer`);

        pageContainer.querySelector(`.subgrid-container`).classList.add(`pp_mainFeed`);

        const rightSidebar = container.querySelector(`#right-sidebar-container`);
        rightSidebar.classList.add(`pp_rightSidebar`);
        rightSidebar.classList.toggle(`styled-scrollbars`, true);

        const originContainer = rightSidebar.parentNode;



        let isWideMode = !(window.innerWidth >= 1392);

        applyRightBar();
        window.addEventListener("resize", (event) => { applyRightBar(); });

        function applyRightBar()
        {
            if(window.innerWidth >= 1392 && !isWideMode)
            {
                pageContainer.prepend(rightSidebar);

                isWideMode = true;
            }

            if(window.innerWidth < 1392 && isWideMode)
            {
                originContainer.append(rightSidebar);
                isWideMode = false;
            }
        }
    }





    // ***********************************************************************************************************************
    // *********************************************** HEADER **************************************************************
    // ***********************************************************************************************************************

    css.addRule('#reddit-logo { text-decoration: none; }');
    css.addRule('.pp_logo { width: max-content; color: var(--shreddit-color-wordmark); font-size: 22px; font-weight: 1000; letter-spacing: -2px; }');

    css.addRule('.pp_userPanel { grid-column: span3 / span3 !important; }');

    renderHeader(document.body);

    function renderHeader(container)
    {
        const nav = container.querySelector(`reddit-header-large`).querySelector(`nav`);

        const userPanel = nav.childNodes.item(4);

        // Firefox first-load fix
        if(awaitElement(userPanel, renderHeader)) return;

        if(checkIsRendered(nav)) return;


        userPanel.classList.add(`pp_userPanel`);
        userPanel.addEventListener(`click`, () => { renderSettingsButton(); }, {once:true});


        const logo = container.querySelector(`#reddit-logo`);
        const logoPP = AppendNew(logo, `div`, `pp_logo`);
        logoPP.textContent = `++`;
    }


    function renderSettingsButton()
    {
        let userMenu = document.getElementById(`user-drawer-content`);

        if(userMenu.querySelector(`faceplate-tracker[noun="pp-settings"]`) != null)
        {
            return;
        }

        let originSettingsButton = userMenu.querySelector(`faceplate-tracker[noun="settings"]`);

        let ppSettingsButton = originSettingsButton.cloneNode(true);
        ppSettingsButton.setAttribute(`noun`, `pp-settings`);

        originSettingsButton.parentNode.appendChild(ppSettingsButton);

        ppSettingsButton.querySelector(`a`).removeAttribute(`href`);

        let path = ppSettingsButton.querySelector(`path`);
        path.setAttribute(`d`, settingsButtonGraphic);

        let text = ppSettingsButton.querySelector(`.text-14`);
        text.textContent = `Reddit++`;

        settingsWindow.onClose = () => {
            if(settingsWindow.changes > 0)
            {
                nextSettingsRevision();

                window.location.reload();
            }
        };

        ppSettingsButton.addEventListener(`click`, () => { settingsWindow.open(); });
    }






    // ***********************************************************************************************************************
    // ********************************************** FEED BUTTONS ***********************************************************
    // ***********************************************************************************************************************

    css.addRule('.pp_feedButtonsContainer { justify-content: space-between; height: 40px !important; }');

    css.addRule('.pp_feedButton { gap: 0.25rem !important;}');
    css.addRule('.pp_feedContainer { gap: 0.25rem; height: 40px; }');


    renderFeedButtons(document.body);

    function renderFeedButtons(container)
    {
        if(settings.isDisabled(SETTING_FEED_BUTTONS)) return;

        const main = container.querySelector(`.main-container`)?.querySelector(`main`);

        // Firefox first-load fix
        if(awaitElement(main, renderFeedButtons)) return;


        const feedDropdown = main.querySelector(`shreddit-sort-dropdown`);

        // skip non feed page
        if(feedDropdown == null && !window.location.href.includes(`/about/`)) return;
        if(window.location.href.includes(`/user/`)) return;

        // get current feed
        const currentFeed = feedDropdown?.querySelector(`div[slot="selected-item"]`)?.textContent;
        feedDropdown?.remove();


        const isHome = window.location.href.includes(`?feed=home`) || window.location.href == `https://www.reddit.com/`;
        const isPopular = window.location.href.includes(`reddit.com/r/popular/`);
        const isAll = window.location.href.includes(`reddit.com/r/all/`);


        // get container
        let buttonsContainer = null;
        let hrefGenerator = null;
        if(isHome || isPopular || isAll)
        {
            const originContainer = main.querySelector(`shreddit-layout-event-setter`).parentNode;
            originContainer.classList.add(`pp_feedButtonsContainer`);

            buttonsContainer = document.createElement(`div`);
            buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
            originContainer.prepend(buttonsContainer);

            if(isHome)
            {
                hrefGenerator = (feed) => { return `/${feed.toLowerCase()}/?feed=home`; };
            }
            else
            {
                hrefGenerator = (feed) => { return `/r/${isPopular ? `popular` : `all`}/${feed.toLowerCase()}/`; };
            }
        }
        else
        {
            const aboutContainer = main.querySelector(`a[slot="page-3"]`)?.parentNode?.parentNode;

            buttonsContainer = document.createElement(`div`);
            buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
            aboutContainer.before(buttonsContainer);

            aboutContainer.remove();


            const subName = getCurrentSub();
            hrefGenerator = (feed) => { return `/r/${subName}/${feed.toLowerCase()}/`; };
        }


        const feeds = (isHome || isPopular) ? FEED_BUTTONS_EXTENDED : FEED_BUTTONS;

        for(const feed of feeds)
        {
            const button = AppendNew(buttonsContainer, `a`, [`inline-flex`, `flex-col`, `text-neutral-content-weak`, `font-semibold`, `rounded-full`, `hover:no-underline`, `hover:bg-secondary-background-hover`, `hover:text-secondary-content`, `active:bg-secondary-background`, `pl-[var(--rem16)]`, `pr-[var(--rem16)]`]);
            button.href = hrefGenerator(feed);

            const isCurrent = feed == currentFeed;
            button.classList.toggle(`bg-secondary-background-selected`, isCurrent);
            button.classList.toggle(`!text-neutral-content-strong`, isCurrent);

            const spanContainer = AppendNew(button, `span`, [`inline-flex`, `flex-row`, `items-center`, `gap-xs`, `py-[var(--rem10)]`, `leading-5`, `font-14`, `pp_feedButton`]);

            let graphic = feedGraphics[feed];
            if(graphic != null)
            {
                let svg = buildSvg(16, 16, graphic);

                spanContainer.append(svg);
            }

            const spanText = AppendNew(spanContainer, `span`);
            spanText.textContent = feed;
        }
    }



    // ***********************************************************************************************************************
    // ************************************************** SUBS ***************************************************************
    // ***********************************************************************************************************************


    renderSub(document.body);

    function renderSub(container)
    {
        // skip page without feed
        const checkIsFeed = container.querySelector(`.main-container`)?.querySelector(`main`)?.querySelector(`shreddit-feed-error-banner`);
        if(checkIsFeed == null) return;

        renderMasthead();

        // flairBar
        awaitSubInfo();
    }


    css.addRule('.masthead > section > div { display: flex; flex-direction: column; align-items: flex-start;}');
    css.addRule('.masthead > section > div > div:last-child { align-self: flex-end;}');

    function renderMasthead()
    {
        const masthead = document.body.querySelector(`.masthead`);

        if(masthead == null) return;

        const section = masthead.querySelector(`section`);
        section.style.top= `-3rem`;
        const div = section.querySelector(`div`);
        div.style.gap = `1rem`;


    }


    function awaitSubInfo()
    {
        const sub = getCurrentSub();
        const subData = subs.get(sub);

        if(subData.flairs != undefined)
        {
            renderSubFlairs(sub);
            return;
        }

        fetch(`https://www.reddit.com/r/${sub}/api/link_flair_v2.json?raw_json=1`, {
            method: `get`
        })
            .then(r => r.json())
            .then(result => {

            if(result != null)
            {
                const loadedFlairs = [];

                for (const loadedFlair of result)
                {
                    const flair = { text:loadedFlair.text, color:loadedFlair.text_color, background:loadedFlair.background_color, richtext: loadedFlair.richtext};

                    loadedFlairs.push(flair);
                }

                subData.flairs = loadedFlairs;


                subs.set(sub, subData);

                renderSubFlairs(sub);
            }
            else
            {
                console.log(`Reddit++: Unable to load subreddit flairs list`);
            }
        });
    }

    css.addRule('.pp_flairMenuContainer { }');
    css.addRule('.pp_flairMenu { display: flex; flex-direction: row; overflow: hidden; }');
    css.addRule('.pp_flairUL { margin-top: 5px !important; flex-wrap: nowrap !important; position: relative; }'); //transition: left 0.2s;

    css.addRule('.pp_flairUL-smoothed { transition: left 0.1s ease-out;}');

    css.addRule('.pp_flairBorderContainer { width: 100%; display: flex; justify-content: space-between; }');

    css.addRule('.pp_flairBorderPrev { width: 20px; }');
    css.addRule('.pp_flairBorder { z-index: 1; position: absolute; height: 40px; width: 20px; margin-top: 5px; }');
    css.addRule('.pp_flairBorderLeft { background: linear-gradient(90deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');
    css.addRule('.pp_flairBorderRight { background: linear-gradient(270deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');

    css.addRule('.pp_flairSettings { color: #434849; padding-top:0.4rem !important; cursor:pointer;}');


    function renderFlair(conatiner, sub, flair)
    {
        const a = AppendNew(conatiner, `a`, `no-decoration`);
        a.href = `/r/`+sub+`/?f=flair_name%3A%22`+flair.text+`%22`;

        const span = AppendNew(a, `span`, [`bg-tone-4`, `inline-block`, `truncate`, `max-w-full`, `text-12`, `font-normal`, `box-border`, `px-[6px]`, `rounded-[20px]`, `leading-4`, `max-w-full`, `py-xs`, `!px-sm`, `leading-4`, `h-xl`, `inline-flex`]);
        span.classList.add(flair.color == `light` ? `text-global-white` : `text-global-black`);
        span.style.backgroundColor = flair.background;

        for(const richElement of flair.richtext)
        {
            if(richElement.e == `text`)
            {
                const content = document.createTextNode(richElement.t);
                span.appendChild(content);
            }
            if(richElement.e == `emoji`)
            {
                const fimg = document.createElement(`faceplate-img`);
                fimg.classList.add(`flair-image`);
                fimg.setAttribute(`loading`, `lazy`);
                fimg.setAttribute(`width`, 16);
                fimg.setAttribute(`height`, 16);
                fimg.setAttribute(`src`, richElement.u);
                fimg.setAttribute(`alt`, richElement.a);
                span.appendChild(fimg);
            }
        }
    }


    function renderSubFlairs(sub)
    {
        if(settings.isDisabled(SETTING_FLAIR_BAR)) return;

        const feedContent = document.body.querySelector(`report-flow-provider`)?.querySelector(`.main-container`)?.querySelector(`shreddit-title`)?.parentNode;

        // skip render for non feed page
        if(feedContent == null) return;

        const prevFlairMenu = feedContent.parentNode.querySelector(`.pp_flairMenuContainer`)
        if(prevFlairMenu != null)
        {
            prevFlairMenu.remove();
        }

        // load data
        const subData = subs.get(sub);
        const flairsData = flairs.get(sub);

        // skip render when sub haven't flairs
        if(subData.flairs == undefined || subData.flairs.length == 0) return;




        const flairMenuContainer = document.createElement(`div`);
        flairMenuContainer.classList.add(`pp_flairMenuContainer`);
        feedContent.before(flairMenuContainer);

        const flairMenu = AppendNew(flairMenuContainer, `div`, `pp_flairMenu`);


        const ul = AppendNew(flairMenu, `ul`, [`p-0`, `m-0`, `list-none`, `gap-xs`, `flex`, `flex-row`, `pp_flairUL`]);
        let flairsRendered = 0;

        for (const flair of subData.flairs)
        {
            if(flairsData.hidden != undefined && flairsData.hidden.includes(flair.text)) continue;

            const li = AppendNew(ul, `li`, `max-w-full`);

            renderFlair(li, sub, flair);


            flairsRendered++;
        }

        // prevent render empty menu
        if(flairsRendered == 0)
        {
            flairMenuContainer.remove();
            return;
        }


        // borders
        const borderContainer = document.createElement(`div`);
        borderContainer.classList.add(`pp_flairBorderContainer`);
        flairMenuContainer.prepend(borderContainer);

        const borderLeftC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
        const borderLeft = AppendNew(borderLeftC, `div`, [`pp_flairBorder`, `pp_flairBorderLeft`]);
        borderLeft.textContent = ` `;
        const borderRightC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
        const borderRight = AppendNew(borderRightC, `div`, [`pp_flairBorder`, `pp_flairBorderRight`]);
        borderRight.textContent = ` `;



        const hr = document.createElement(`hr`);
        hr.classList.add(`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`);
        flairMenuContainer.prepend(hr);

        const mymx = document.createElement(`div`);
        mymx.classList.add(`my-xs`, `mx-2xs0`);
        flairMenuContainer.prepend(mymx);


        // navigation
        ul.style.left = `25px`;
        const ulRect = ul.getBoundingClientRect();
        const menuRect = flairMenu.getBoundingClientRect();

        flairMenu.addEventListener(`mousemove`, e => {onMoveOverFlairs(e, ul, flairMenu)});

        if(ulRect.width > menuRect.width * 1.72)
        {
            ul.classList.add(`pp_flairUL-smoothed`);
        }
    }


    function onMoveOverFlairs(e, ul, flairMenu)
    {
        const ulRect = ul.getBoundingClientRect();
        const menuRect = flairMenu.getBoundingClientRect();

        if(ulRect.width < menuRect.width)
        {
            ul.style.left = `25px`;
            return;
        }

        let scale = (e.clientX - (menuRect.x + 25)) / ((menuRect.right - 25) - (menuRect.x + 25));
        scale = Math.max(0, Math.min(scale, 1));

        ul.style.left = `${Math.round(25 - (ulRect.width - menuRect.width + 50) * scale)}px`;
    }


    //************ Flairs Settings *************

    css.addRule('.pp_WindowScrollContent { overflow-y: scroll;  }');

    css.addRule('.pp_checkBoxContainer { float: right; position: relative; }');

    css.addVar('--checkBox-background', '#1a1a1b1a', '#81818152');
    css.addRule('.pp_checkBoxButtonActive { justify-content: flex-end; background-color: #0079d3; }');
    css.addRule('.pp_checkBoxButton { position: relative; cursor:pointer; overflow:visible; display:flex; justify-content: start; background: transparent; background-color: var(--checkBox-background); padding: initial;  height: 24px; width:37.5px; border-radius: 100px; border: 2px solid transparent; transition: background-color 0.2s linear; }');

    css.addRule('.pp_checkBoxKnob { height: 19.5px; width:19.5px; background-color: #fff;  box-shadow: 0 0 0 1px rgba(0,0,0,.1),0 2px 3px 0 rgba(0,0,0,.2); transition: 0.5s linear; border-radius: 57%; }');


    css.addRule('.pp_fs_flairContainer { width: 100%; display: flex; align-items: center; margin-left: 3rem; }');
    css.addRule('.pp_fs_flairPanelCheckboxArea { width: 200px; min-width: 200px; display: flex; justify-content: center; align-items: center; }');

    css.addRule('.pp_fs_columnTittle { margin: 20px 57px 10px 40px; }');



    function renderFlairsWindow(win, context)
    {
        const titlePanel = AppendNew(win.content, `div`, [`pp_WindowElement`, `pp_fs_columnTittle`]);

        const tittleBarArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
        const tittleBar = AppendNew(tittleBarArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
        tittleBar.textContent = `show on bar:`;
        const tittleFeedArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
        const tittleFeed = AppendNew(tittleFeedArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
        tittleFeed.textContent = `show in feed:`;


        const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);

        const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);

        const subData = subs.get(context.sub);
        for (const flair of subData.flairs)
        {
            const panel = AppendNew(elements, `div`, `pp_WindowElement`);

            const flairContainer = AppendNew(panel, `div`, `pp_fs_flairContainer`);

            addFlairToggle(`hidden`);
            addFlairToggle(`banned`);


            function addFlairToggle(target)
            {
                const checkboxArea = AppendNew(panel, `div`, `pp_fs_flairPanelCheckboxArea`);
                const checkBoxContainer = AppendNew(checkboxArea, `div`, `pp_checkBoxContainer`);
                const checkBoxBack = AppendNew(checkBoxContainer, `button`, `pp_checkBoxButton`);
                const initState = !(flairs.get(context.sub)[target]?.includes(flair.text) ?? false);
                checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, initState);
                const knob = AppendNew(checkBoxBack, `div`, `pp_checkBoxKnob`);


                checkBoxBack.addEventListener(`click`, e => {
                    const flairData = flairs.get(context.sub);

                    let state = flairData[target]?.includes(flair.text) ?? false;

                    checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, state);

                    if(state)
                    {
                        flairData[target] = flairData[target].filter(f => f != flair.text);
                    }
                    else
                    {
                        const targetList = flairData[target] ?? [];
                        targetList.push(flair.text);
                        flairData[target] = targetList;
                    }

                    flairs.set(context.sub, flairData);
                });
            }

            // flair
            renderFlair(flairContainer, context.sub, flair);

        }
    }



    // *********** Sub ContextMenu *************

    document.body.addEventListener(`click`, clickSubredditMenu);

    function clickSubredditMenu(e)
    {
        if(e.target.matches(`shreddit-subreddit-header-buttons`) == true)
        {
            if(checkIsRendered(e.target)) return;

            const controlMenu = e.target.shadowRoot.querySelector(`shreddit-subreddit-overflow-control`).shadowRoot.querySelector(`faceplate-menu`);

            const originButton = controlMenu.querySelector(`li`);

            // flairs settings
            const menuFlairsButton = originButton.cloneNode(true);
            menuFlairsButton.querySelector(`.text-14`).textContent = `Flairs settings`;
            controlMenu.prepend(menuFlairsButton);

            const sub = getCurrentSub();

            flairsWindow.onClose = () => { renderSubFlairs(sub); };

            menuFlairsButton.addEventListener(`click`, () => { flairsWindow.open({sub:sub}); });

            // about
            const link = document.createElement(`a`);
            link.href = `https://www.reddit.com/`+e.target.getAttribute(`prefixed-name`)+`/about/`;
            link.classList.add(`no-underline`);
            controlMenu.prepend(link);

            const menuAboutButton = originButton.cloneNode(true);
            menuAboutButton.querySelector(`.text-14`).textContent = `About`;
            link.prepend(menuAboutButton);
        }
    }




    // ***********************************************************************************************************************
    // ************************************************* POSTS ***************************************************************
    // ***********************************************************************************************************************

    css.addRule('.pp_bannedPost { visibility: hidden !important;  max-height: 0px;}');


    checkNativePosts(document.body);

    function checkNativePosts(container)
    {
        const main = container.querySelector(`main`);

        if(main != null)
        {
            main.querySelectorAll(`shreddit-post`).forEach((post) => {
                renderPost(post);
            });
        }

    }

    function renderPost(post)
    {
        const sub = post.getAttribute(`subreddit-prefixed-name`).replace(`r/`, ``);
        const flairData = flairs.get(sub);

        const postFlair = post.querySelector(`shreddit-post-flair`)?.querySelector(`a`);
        if(postFlair != null)
        {
            const postFlairText = decodeURIComponent(postFlair.href.split(`%22`)[1]);

            if(flairData.banned != undefined && flairData.banned.includes(postFlairText))
            {
                const next = post.parentElement.nextElementSibling;
                next.classList.add(`pp_bannedPost`);
                post.parentElement.classList.add(`pp_bannedPost`);
                post.classList.add(`pp_bannedPost`);
                post.querySelector(`faceplate-tracker[source="post_credit_bar"]`).classList.add(`pp_bannedPost`);
            }
        }
    }







    // ***********************************************************************************************************************
    // *********************************************** COMMENTS **************************************************************
    // ***********************************************************************************************************************

    const NEWUSER_SECONDS_SHIFT = DAY_SECONDS * 64;
    const REFRESH_USER_SECONDS_SHIFT = DAY_SECONDS;
    const BLOCK_COOLDOWN_SECONDS = DAY_SECONDS + 42;

    let loadingComments = {};
    let currentLoads = 0;


    function registryAllComments(container)
    {
        container.querySelectorAll(`shreddit-comment`).forEach((comment) => {
            registryComment(comment);
        });
    }


    function registryComment(comment)
    {
        if(checkIsRendered(comment)) return;

        commentRenderIntersector.observe(comment.querySelector(`div[slot="commentMeta"]`));
    }

    css.addRule('.pp_muted_avatar { opacity: 0.5; }');
    css.addRule('.pp_muted_content { color: #a5a5a5; transition: color 0.2s; }');
    css.addRule('.pp_muted_content:hover { color: var(--color-tone-1); }');

    function renderComment(comment)
    {
        // add anchors
        const nickname = comment.querySelector(`div[slot="commentMeta"]`).querySelector(`faceplate-hovercard[data-id="user-hover-card"]`);

        // skip [deleted]
        if(nickname == null) return;

        const tagsAnchor = document.createElement(`div`);
        tagsAnchor.setAttribute(`pp-anchor`, `tags`);
        nickname.parentNode.querySelector(`.ml-2xs`).after(tagsAnchor);

        const time = nickname.parentNode.querySelector(`time`).parentNode.parentNode;

        const infoAnchor = document.createElement(`div`);
        infoAnchor.setAttribute(`pp-anchor`, `info`);
        time.before(infoAnchor);

        // make ghosted when karma below zero
        if(settings.isEnabled(SETTING_GHOSTED_COMMENTS) && comment.getAttribute(`score`) < 0)
        {
            comment.querySelector(`div[slot="commentAvatar"]`).classList.add(`pp_muted_avatar`);
            comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`).style.color = `#a5a5a5`;
            comment.querySelector(`div[slot="comment"]`).classList.add(`pp_muted_content`);

        }

        // registry image
        const image = comment.querySelector(`div[slot="comment"]`).querySelector(`faceplate-img`);
        if(image != null)
        {
            registryImage(image);
        }


        renderCommentTags(comment);

        awaitContextMenu(comment);

        awaitUserInfo(comment);
    }

    css.addRule('.pp_imageViewable { cursor: pointer; }');

    function registryImage(imgContainer)
    {
        imgContainer.classList.add(`pp_imageViewable`);
        imgContainer.parentNode.removeAttribute(`href`);
        imgContainer.addEventListener(`click`, () => { imageViewer.open(imgContainer.getAttribute(`src`)); });
    }


    function renderCommentTags(comment)
    {
        if(settings.isDisabled(SETTING_USER_TAGS)) return;

        const userId = comment.getAttribute(`author`);
        if(userId == null) return;

        const tagsData = tags.get(userId);

        const tagsContainer = comment.querySelector(`div[pp-anchor="tags"]`);

        // skip not ininitialized comments
        if(tagsContainer == null) return;

        // clear old tags
        tagsContainer.parentNode.querySelectorAll(`svg[tag="true"]`).forEach(tag => { tag.remove(); });

        if(tagsData.tags != undefined)
        {
            for(const tag of tagsData.tags)
            {
                renderTag(tag);
            }
        }

        function renderTag(tag)
        {
            let tagSvg = buildSvg(20, 20, tagGraphics[tag], {w:1.5, f:`none`});
            tagSvg.setAttribute(`tag`, `true`);
            tagSvg.setAttribute(`viewBox`, `-4 -4 20 20`);
            tagSvg.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
            tagsContainer.after(tagSvg);
        }
    }

    function awaitUserInfo(comment)
    {
        const userId = comment.getAttribute(`author`);

        let userData = users.get(userId);

        if(userData.accountId == undefined)
        {
            loadUserInfo(comment, userId);
            return;
        }

        renderUserInfo(comment);
    }

    function loadUserInfo(comment, userId)
    {
        if(comment == null)
        {
            // do nothing
        }
        else if(loadingComments[userId] != undefined && loadingComments[userId] != null)
        {
            loadingComments[userId].push(comment);
            return;
        }
        else
        {
            loadingComments[userId] = [comment];
            currentLoads++;
        }


        fetch(`https://oauth.reddit.com/user/${userId}/about.json`, {
            cache: `no-cache`,
            method: `get`
        })
            .then(r => r.json())
            .then(result => {

            if(result != null)
            {
                let userData = users.get(userId);

                userData.rating = result.data.link_karma + result.data.comment_karma / 2;
                if(result.data.subreddit.title)
                {
                    userData.nick = result.data.subreddit.title;
                }
                userData.created = result.data.created;
                userData.accountId = result.kind + `_` + result.data.id;

                users.set(userId, userData);


                for (let comment of loadingComments[userId])
                {
                    renderUserInfo(comment);
                }

                loadingComments[userId] = null;
                currentLoads--;

                if(currentLoads == 0)
                {
                    loadingComments = {};
                }
            }
        });
    }

    // TODO: need replace vars in top
    function renderUserInfo(comment)
    {
        let nickName = comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`);

        const userId = comment.getAttribute(`author`);

        let userData = users.get(userId);


        if(settings.isEnabled(SETTING_SHOW_NAMES) && userData.nick != undefined && userData.nick)
        {
            nickName.textContent = userData.nick;
        }

        // skip render user info
        if(settings.isDisabled(SETTING_USER_INFO)) return;

        const infoAnchor = comment.querySelector(`div[pp-anchor="info"]`);


        const rating = document.createElement(`div`);
        rating.classList.add(`text-neutral-content-weak`, `text-12`);

        rating.textContent = (userData.rating < 10000) ? `${Math.round(userData.rating / 100) / 10}K` : `${Math.round(userData.rating / 1000)}K`;
        infoAnchor.after(rating);

        const point = document.createElement(`span`);
        point.classList.add(`inline-block`, `my-0`, `mx-2xs`, `text-12`, `text-neutral-content-weak`);
        point.textContent = `•`;
        rating.after(point);

        if(userData.created > (Date.now()/1000 - NEWUSER_SECONDS_SHIFT))
        {
            const tagsAnchor = comment.querySelector(`div[pp-anchor="tags"]`);

            const newSvg = buildSvg(20, 20, newUserGraphic);
            newSvg.setAttribute(`viewBox`, `-2 -2 20 20`);
            newSvg.style.color = `#69b508`;

            tagsAnchor.before(newSvg);
        }

    }


    function awaitContextMenu(comment)
    {
        let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`)?.shadowRoot?.querySelector(`faceplate-dropdown-menu`);

        if(contextMenuButton == null)
        {
            setTimeout(() => { awaitContextMenu(comment); }, 50);
            return;
        }

        contextMenuButton.addEventListener(`click`, () => { renderContextMenu(comment); }, {once:true});
    }


    css.addRule('.pp_tagsPanel { display: flex; justify-content: space-around; width:auto; border-bottom: solid 1px var(--color-neutral-border-weak); padding: 4px; gap: 8px; margin-bottom: 4px; }');

    css.addRule('.pp_tagButton { cursor:pointer; display: flex; align-content: center; flex-wrap: wrap; height:45px; padding: 4px 20px; margin: 0px 0px; color: var(--color-neutral-border-weak); border-radius: 5px; }');
    css.addRule('.pp_tagButton svg { width:20px; transition: transform 0.15s; }');
    css.addRule('.pp_tagButton:hover svg { transform: scale(1.2, 1.2); transition: transform 0.3s; }');
    css.addRule('.pp_tagButton:hover { background-color: var(--color-neutral-background-hover);  }');
    css.addRule('.pp_tagButtonActive:hover { opacity: 0.8; }');

    css.addRule('.pp_tagHintOffset { left: 50%; position:absolute; }');
    css.addRule('.pp_tagHintContainer { display: flex; justify-content: center; position:fixed; }');
    css.addRule('.pp_tagHint { display: flex; align-items: center; position: absolute; top: -35px; height: 25px; padding: 0px 12px; color: var(--color-neutral-background-weak); font: var(--font-small); background-color: var(--color-neutral-content-strong); border-radius: 5px;}');



    function renderContextMenu(comment)
    {
        // close other context menu
        document.body.click();

        let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`).shadowRoot;

        let originButton = contextMenuButton.querySelector(`faceplate-tracker[noun="report"]`);
        let contextMenuContainer = originButton.parentNode;

        if(settings.isEnabled(SETTING_HIDE_SHARE))
        {
            let linkButton = originButton.cloneNode(true);
            linkButton.querySelector(`span .text-14`).textContent = `Copy link`;
            originButton.before(linkButton);

            let linkPath = linkButton.querySelector(`path`);
            linkPath.setAttribute(`d`, linkGraphics[0].d);
            let linkPathB = linkPath.cloneNode(true);
            linkPathB.setAttribute(`d`, linkGraphics[1].d);
            linkPath.after(linkPathB);

            const permalink = comment.getAttribute(`permalink`);
            linkButton.addEventListener(`click`, () => {
                navigator.clipboard.writeText(`https://www.reddit.com${permalink}`);
                notify(`Link copied`);
            });
        }


        css.registry(contextMenuButton);

        // close context menu
        let openButton = contextMenuButton.querySelector(`button`);
        openButton.addEventListener(`click`, () => {document.body.click(); });


        // skip render tags
        if(settings.isDisabled(SETTING_USER_TAGS)) return;

        const tagHintOffset = document.createElement(`div`);
        tagHintOffset.classList.add(`pp_tagHintOffset`);
        contextMenuContainer.prepend(tagHintOffset);

        const tagHintContainer = document.createElement(`div`);
        tagHintContainer.classList.add(`pp_tagHintContainer`);
        tagHintOffset.prepend(tagHintContainer);

        const tagHint = AppendNew(tagHintContainer, `div`, `pp_tagHint`);
        tagHint.style.display = `none`;


        const tagsPanel = document.createElement(`div`);
        tagsPanel.classList.add(`pp_tagsPanel`);
        tagHintOffset.after(tagsPanel);

        const userId = comment.getAttribute(`author`);

        renderTagButton(USERTAG_FOLLOWED);
        renderTagButton(USERTAG_LIKED);
        renderTagButton(USERTAG_WARNING);
        renderTagButton(USERTAG_BLOCKED);

        function renderTagButton(tag)
        {
            const tagButton = AppendNew(tagsPanel, `span`, `pp_tagButton`);
            tagButton.setAttribute(`tag`, tag);

            const subscribeIcon = buildSvg(20, 20, tagButtonGraphics[tag], {w:2, f:`none`});
            tagButton.appendChild(subscribeIcon);

            tagButton.addEventListener(`click`, () => { tagButtonClick(userId, tag, tagButton); });
            tagButton.addEventListener(`mouseenter`, () => {tagHintEnter(userId, tag, tagButton, tagHint); });
            tagButton.addEventListener(`mouseleave`, () => {tagHintOut(userId, tag, tagButton, tagHint); });
        }

        refreshTagButtons(tagsPanel, userId);

    }

    function refreshTagButtons(tagsPanel, userId)
    {
        const tagsData = tags.get(userId);
        const tagsList = tagsData?.tags ?? [];

        tagsPanel.querySelectorAll(`.pp_tagButton`).forEach(button => {
            const tag = button.getAttribute(`tag`);

            button.removeAttribute(`has-cooldown`);
            button.removeAttribute(`has-blocked`);

            if(tagsList.includes(tag))
            {
                button.classList.add(`pp_tagButtonActive`);
                button.style.backgroundColor = tagConfigs[tag][TAG_CONFIG_COLOR];
                button.style.color = `white`;
            }
            else
            {
                button.classList.remove(`pp_tagButtonActive`);
                button.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
                button.style.removeProperty(`background-color`);

                if(tag == USERTAG_BLOCKED && tagsData.blockCooldown != undefined && Date.now()/1000 < tagsData.blockCooldown)
                {
                    button.setAttribute(`has-cooldown`, ``);
                    button.style.color = `#adadad`;
                }

                if(tag == USERTAG_FOLLOWED && tagsList.includes(USERTAG_BLOCKED))
                {
                    button.setAttribute(`has-blocked`, ``);
                    button.style.color = `#adadad`;
                }
            }
        });
    }


    function tagButtonClick(userId, tag, button)
    {
        if(button.getAttribute(`has-cooldown`) != null || button.getAttribute(`has-blocked`) != null)
        {
            notify(`Unable to do this`);
            return;
        }

        let tagsData = tags.get(userId);

        if(tagsData.tags == undefined)
        {
            tagsData.tags = [];
        }

        let isAdded = false;
        if(tagsData.tags.includes(tag))
        {
            tagsData.tags = tagsData.tags.filter(t => t != tag);
        }
        else
        {
            tagsData.tags.push(tag);
            isAdded = true;

            // auto clear follow state
            if(tag == USERTAG_BLOCKED)
            {
                tagsData.tags = tagsData.tags.filter(t => t != USERTAG_FOLLOWED);
            }
        }

        if(tagsData.tags.length > 1)
        {
            tagsData.tags.sort((firstItem, secondItem) => tagConfigs[firstItem][TAG_CONFIG_PRIORITY] - tagConfigs[secondItem][TAG_CONFIG_PRIORITY]);
        }

        tags.set(userId, tagsData);

        // refresh comments tags
        document.body.querySelectorAll(`shreddit-comment[author="${userId}"]`).forEach(comment => {
            renderCommentTags(comment);

            if(isAdded && tag == USERTAG_BLOCKED)
            {
                comment.setAttribute(`collapsed`, ``);
            }
        });

        // execute specific operations
        if(tag == USERTAG_FOLLOWED)
        {
            executeOperation(`UpdateProfileFollowState`, isAdded ? `FOLLOWED` : `NONE`, userId);
        }

        if(tag == USERTAG_BLOCKED)
        {
            if(!isAdded)
            {
                tagsData.blockCooldown = Date.now()/1000 + BLOCK_COOLDOWN_SECONDS;
                tags.set(userId, tagsData);
            }

            executeOperation(`UpdateRedditorBlockState`, isAdded ? `BLOCKED` : `NONE`, userId);
        }

        // refresh context menu
        refreshTagButtons(button.parentNode, userId);

    }

    function tagHintEnter(userId, tag, button, tagHint)
    {
        tagHint.style.display = null;

        tagHint.dataset.target = tag;

        const tagsData = tags.get(userId);

        tagHint.innerText = tagConfigs[tag][(tagsData?.tags ?? []).includes(tag) ? TAG_CONFIG_HINT_ACTIVATED : TAG_CONFIG_HINT];

        if(button.getAttribute(`has-cooldown`) != null)
        {
            const cooldownHours = Math.round((tagsData.blockCooldown - Date.now()/1000) / HOUR_SECONDS);
            tagHint.innerText = `Unable block for ${cooldownHours}h after unblocking`;
        }

        if(button.getAttribute(`has-blocked`) != null)
        {
            tagHint.innerText = `Unable to follow on blocked user`;
        }
    }

    function tagHintOut(userId, tag, button, tagHint)
    {
        if(tagHint.dataset?.target == tag)
        {
            tagHint.style.display = `none`;
        }
    }


    function executeOperation(operation, state, userId)
    {
        let userData = users.get(userId);

        const body = {
            csrf_token:getCookie(`csrf_token`),
            operation:operation,
            variables:{
                input: operation == `UpdateRedditorBlockState` ? {
                    redditorId:userData.accountId,
                    blockState:state
                } : {
                    accountId:userData.accountId,
                    state:state,
                }
            }
        };

        fetch(`https://www.reddit.com/svc/shreddit/graphql`, {
            method: `post`,
            headers: new Headers({
                'Accept': `application/json`,
                'Content-Type': `application/json`
            }),
            body: JSON.stringify(body)
        })
            .then(r => r.json())
            .then(result => {

            if(result != null && result.errors?.message)
            {
                notify(result.errors.message);
            }

        });
    }







    // ***********************************************************************************************************************
    // *********************************************** SETTINGS **************************************************************
    // ***********************************************************************************************************************

    css.addRule(' .pp_SettingSubtittle { font-size: 10px; font-weight: 700; letter-spacing: 0.5px; line-height: 12px; min-height: 20px; color: #7c7c7c; text-transform: uppercase; border-bottom: 1px solid #edeff1;  margin-top: 1rem; padding: 0rem 3rem;}');


    css.addRule(' .pp_SettingPropertyHeader { display: flex; flex-direction: column; margin-left: 3rem; justify-content: center; }');

    css.addRule(' .pp_SettingPropertyHeaderTittle { font-size: 16px; font-weight: 500; line-height: 20px; margin-bottom: 4px;  }');
    css.addRule(' .pp_SettingPropertyHeaderDescription { font-size: 12px; font-weight: 400; line-height: 16px; color: #7c7c7c; }');
    css.addRule(' .pp_SettingPropertyButtonContainer { display: flex; align-items: center; justify-content:flex-end; flex-grow:1; }');


    css.addRule(' .pp_SettingChangesBannerContainer { position: absolute; top: 0px; width: 900px; overflow-y: hidden; opacity: 0; transition: opacity 0.15s ease-in-out; }');
    css.addRule(' .pp_SettingChangesBanner { display: flex; justify-content: center;  margin: 2rem 15%; padding: 1rem; border-radius: 15px; background-color: #ffd40017; border: solid 1px #ffd400; color: #d7b300; font-weight: 500; }');
    css.addRule(' .pp_SettingChangesBannerActive { opacity: 1 !important; }');

    function renderSettingsWindow(win, context)
    {
        // close user context menu
        document.body.click();

        win.changes = 0;


        const changesBannerContainer = AppendNew(win.content, `div`, `pp_SettingChangesBannerContainer`);
        const changesBanner = AppendNew(changesBannerContainer, `div`, `pp_SettingChangesBanner`);
        changesBanner.textContent = `Page will be reloaded to apply new settings`;


        const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);

        const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);


        addSettingToggle(`Wide mode`, `Fit sidebars to screen borders and limit content width to 700px`, SETTING_WIDE_MODE);

        addSubtittle(`Feed`);


        addSettingToggle(`Feed buttons`, `Unwrap feed sorting buttons`, SETTING_FEED_BUTTONS);
        addSettingToggle(`Flair bar`, `Display available flairs to faster navigation. Specific flairs may be hidden in subreddit flairs settings.`, SETTING_FLAIR_BAR);

        addSettingToggle(`Soft backplates`, `Make post backplates with gradient color`, SETTING_BACKPLATES);

        addSubtittle(`Comments`);

        addSettingToggle(`User info`, `Show user karma and leaf mark for new user`, SETTING_USER_INFO);
        addSettingToggle(`User tags`, `Enable custom tags for users`, SETTING_USER_TAGS);

        addSettingToggle(`Show names`, `Use display names (if user set it) instead usernames`, SETTING_SHOW_NAMES);
        addSettingToggle(`Bigger fonts`, `Make fonts bigger for better reading`, SETTING_BIGGER_FONTS);
        addSettingToggle(`Hide share button`, `Replace share button to context menu`, SETTING_HIDE_SHARE);
        addSettingToggle(`Ghosted comments`, `Make comments ghosted when karma below zero`, SETTING_GHOSTED_COMMENTS);



        function addSubtittle(text)
        {
            const subtittle = AppendNew(elements, `h3`, `pp_SettingSubtittle`);
            subtittle.textContent = text;
        }

        function addSettingToggle(tittleText, descriptionText, settingName)
        {
            const propertyArea = AppendNew(elements, `div`, `pp_WindowElement`);

            const header = AppendNew(propertyArea, `div`, `pp_SettingPropertyHeader`);
            const tittle = AppendNew(header, `div`, `pp_SettingPropertyHeaderTittle`);
            tittle.textContent = tittleText;
            if(descriptionText != null)
            {
                const description = AppendNew(header, `div`, `pp_SettingPropertyHeaderDescription`);
                description.textContent = descriptionText;
            }

            const buttonContainer = AppendNew(propertyArea, `div`, `pp_SettingPropertyButtonContainer`);

            const toggleArea = AppendNew(buttonContainer, `div`, `pp_fs_flairPanelCheckboxArea`);
            const toggleContainer = AppendNew(toggleArea, `div`, `pp_checkBoxContainer`);
            const toggleBack = AppendNew(toggleContainer, `button`, `pp_checkBoxButton`);
            toggleBack.classList.toggle(`pp_checkBoxButtonActive`, settings.get(settingName) != false);
            const knob = AppendNew(toggleBack, `div`, `pp_checkBoxKnob`);

            let changed = false;

            toggleBack.addEventListener(`click`, e => {

                let state = !(settings.get(settingName) == false);

                toggleBack.classList.toggle(`pp_checkBoxButtonActive`, !state);

                settings.set(settingName, !state);

                win.changes += changed ? -1 : 1;
                changed = !changed;

                changesBannerContainer.classList.toggle(`pp_SettingChangesBannerActive`, win.changes > 0);
            });
        }

    }





    // ***********************************************************************************************************************
    // ************************************************* UTILS ***************************************************************
    // ***********************************************************************************************************************

    function getCurrentSub()
    {
        const raw = window.location.href.split(`reddit.com/r/`);
        return raw.length > 1 ? raw[1].split(`/`)[0] : null;
    }

    function checkIsRendered(node)
    {
        if(node.getAttribute(`pp-rendered`) != null)
        {
            return true;
        }
        else
        {
            node.setAttribute(`pp-rendered`, ``);
            return false;
        }
    }

    function notify(message, actionTittle, actionCallback)
    {
        const hasAction = actionTittle != undefined;

        let toaster = document.body?.querySelector(`alert-controller`)?.shadowRoot?.querySelector(`toaster-lite`);

        if(toaster == null)
        {
            console.log(`Reddit++: Failed to notify (${message})`);
            return;
        }

        let toast = document.createElement(`faceplate-toast`);
        toast.classList.add(`theme-rpl`);
        toast.textContent = message;

        toaster.appendChild(toast);

        setTimeout(() => {toast.setAttribute(`_fading`, ``); }, hasAction ? 8000 : 3000);
    }


    function buildSvg(w, h, graphics, settings)
    {
        const XMLNS = `http://www.w3.org/2000/svg`;
        const svg = document.createElementNS(XMLNS, `svg`);
        svg.setAttribute(`viewBox`, `0 0 ${w} ${h}`);
        svg.setAttribute(`width`, `${w}px`);
        svg.setAttribute( `height`, `${h}px`);
        svg.setAttribute(`fill`, `none`);

        for(var graphic of graphics)
        {
            let path = document.createElementNS(XMLNS, `path`);
            path.setAttribute(`stroke-linecap`, `round`);
            path.setAttribute(`stroke-linejoin`, `round`);
            path.setAttribute(`stroke`, graphic?.c ?? (settings?.c ?? `currentColor`));
            path.setAttribute(`stroke-width`, graphic?.w ?? (settings?.w ?? 1));
            path.setAttribute(`fill`, graphic?.f ?? (settings?.f ?? `currentColor`));
            path.setAttribute(`d`, graphic.d);
            svg.appendChild(path);
        }

        return svg;
    }

    function AppendNew(prev, name, classes)
    {
        const el = document.createElement(name);

        if(classes != undefined)
        {
            if(typeof classes === `string` && classes)
            {
                el.classList.add(classes);
            }
            else
            {
                for(const c of classes)
                {
                    el.classList.add(c);
                }
            }
        }

        prev.append(el);
        return el;
    }

    function getSettingsRevision()
    {
        return localStorage.getItem(`pp_settings_revision`) ?? 0;
    }

    function nextSettingsRevision()
    {
        settingsRevision++;
        localStorage.setItem(`pp_settings_revision`, settingsRevision);
    }

    function getCookie(key)
    {
        return document.cookie
            .split(`; `)
            .find((row) => row.startsWith(key))
            ?.split(`=`)[1];
    }

    function awaitElement(element, callback)
    {
        if(element == null)
        {
            setTimeout(() => { callback(document.body); }, 100);
            return true;
        }

        return false;
    }


})();