BetterLM

make Linkomanija great again

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         BetterLM
// @namespace    https://blm.hades.lt
// @version      1.8.4
// @description  make Linkomanija great again
// @author       Krupp
// @match        https://www.linkomanija.net/*
// @grant        none
// ==/UserScript==

//--- utils.js
class Utils {
        static InsertAfter(element, newNode) {
                return element.parentElement.insertBefore(newNode, element.nextSibling);
        }

        static InsertBefore(element, newNode) {
                return element.parentElement.insertBefore(newNode, element.previousSibling);
        }

        static GetSelectedValues(element) {
                let res = [];

                if (element.options) {
                        for (let i = 0; i < element.options.length; i++) {
                                let opt = element.options[i];
                                if (opt.selected)
                                        res.push(opt.value);
                        }
                }

                return res;
        }

        static async FetchJSON(url, method = 'GET', data = null) {
                let reqObj = {method};
                if (data) {
                        reqObj.body = JSON.stringify(data);
                        reqObj.headers = {'content-type': 'application/json'};
                }

                return await fetch(Settings.ApiUrl + url, reqObj).then(resp => resp.json());
        }

        static PrintDate(dateStr) {
                if (!dateStr) return '';

                // do not convert to UTC
                dateStr = dateStr.replace('T', ' ');
                let date = new Date(dateStr);

                return `${date.getFullYear()}-${Utils._WZero(date.getMonth() + 1)}-${Utils._WZero(date.getDate())}
${Utils._WZero(date.getHours())}:${Utils._WZero(date.getMinutes())}:${Utils._WZero(date.getSeconds())}`;
        }

        // with zero, so it outputs 09 minutes instead of 9 -.-
        static _WZero(inp) {
                return inp < 10 ? '0' + inp : '' + inp; // so that it always returns String, not Number sometimes
        }

        // Null or Undefined
        static NU(obj) {
                return obj === null || obj === undefined;
        }

        static GetPosts() {
                return document.querySelectorAll('div[id^="post_"]');
        }

        static GetPostId(postEl) {
                if (!postEl) return null;

                let postIdAttr = postEl.getAttribute('id');

                if (postIdAttr) {
                        postIdAttr = postIdAttr.replace('post_', '');
                        let postId = Number(postIdAttr);

                        if (Number.isNaN(postId)) return null;
                        else return postId;
                }

                return null;
        }

        static get _postDateRegex() {
                return /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;
        }

        static GetPostDate(postEl) {
                let infoTextEl = postEl.querySelector('span:first-child');
                if (infoTextEl) {
                        let regexResult = Utils._postDateRegex.exec(infoTextEl.textContent);
                        if (regexResult) return new Date(regexResult[0]);
                }

                return null;
        }
}

//--- options.js
class Options {
        constructor(obj = null) {
                if (obj) {
                        this.notifyOnUserMention = obj.notifyOnUserMention;
                        this.showLastPosts = obj.showLastPosts;
                        this.ignoreHideTopics = obj.ignoreHideTopics;
                        this.ignoreReplaceMessages = obj.ignoreReplaceMessages;
                        this.ignoreReplaceString = obj.ignoreReplaceString;
                }
        }

        validate() {
                if (Utils.NU(this.notifyOnUserMention) || Utils.NU(this.showLastPosts)
                        || Utils.NU(this.ignoreReplaceMessages) || Utils.NU(this.ignoreHideTopics))
                        throw "options aren't valid";
        }

        setDefaults() {
                this.notifyOnUserMention = true;
                this.showLastPosts = false;
                this.ignoreHideTopics = false;
                this.ignoreReplaceMessages = true;
                this.ignoreReplaceString = 'Vartotojas ignoruotas.';
        }
}

//--- settings.js
class Settings {
        static get KeyOptions() {
                return 'blm_options';
        }

        static get KeyIgnored() {
                // for backwards compatibility with 'LMRetard' script
                return 'retards';
        }

        static get ApiUrl() {
                return 'https://blm.hades.lt';
        }

        static Instance() {
                if (this._instance)
                        return this._instance;

                this._instance = new Settings();
                this._instance._load();
                return this._instance;
        }

        save() {
                let optionsJson = JSON.stringify(this.options);
                localStorage.setItem(Settings.KeyOptions, optionsJson);

                let ignoredJson = JSON.stringify(this.ignored);
                localStorage.setItem(Settings.KeyIgnored, ignoredJson);
        }

        _load() {
                // load options
                try {
                        let optionsJson = localStorage.getItem(Settings.KeyOptions);
                        let options = JSON.parse(optionsJson);

                        this.options = new Options(options);
                        this.options.validate();
                }
                catch (ex) {
                        // oh well...
                        this._resetOptions();
                        this.save();
                }

                // load ignored users
                try {
                        let ignoredJson = localStorage.getItem(Settings.KeyIgnored);
                        let ignored = JSON.parse(ignoredJson);

                        if (!Array.isArray(ignored))
                                throw 'do not swear';

                        ignored.forEach(x => {
                                if (typeof x !== 'string')
                                        throw 'fuck, I told a swear word';
                        });

                        this.ignored = ignored;
                }
                catch (ex) {
                        // well shitballs...
                        this._resetIgnored();
                        this.save();
                }

                // set own name
                let username = document.querySelector('#username');
                if (username)
                        this.ownName = username.innerText;

                // set own id
                let userHref = username.querySelector('a');
                if (userHref)
                        this.ownUserId = Number(userHref.href.split('?id=')[1]); // 99% of the time never throws
        }

        isIgnored(name) {
                if (name === this.ownName)
                        return false;

                return this.ignored.indexOf(name) !== -1;
        }

        addIgnored(name) {
                if (name === this.ownName)
                        return;

                if (!this.isIgnored(name)) {
                        this.ignored.push(name);
                        this.save();
                } else {
                        // todo: something better than alert would be nice
                        alert(`${name} is already ignored, refresh the page.`);
                }
        }

        getIgnored() {
                return this.ignored;
        }

        removeIgnored(name, save = true) {
                let idx = this.ignored.indexOf(name);

                if (idx !== -1) {
                        this.ignored.splice(idx, 1);
                        if (save)
                                this.save();
                }
        }

        _resetOptions() {
                this.options = new Options();
                this.options.setDefaults();
        }

        _resetIgnored() {
                this.ignored = [];
        }
}

//--- base.js
class Base {
        constructor() {
                this.settings = Settings.Instance();
        }
}

//--- templates/templateEngine.js
// "Holy Mashed Potatoes, Batman!" -Robin
// update: fuck this shit, shoulda used handlebars
class TemplateEngine {
        static get _ForeachEnd() {
                return '@{/foreach}';
        }

        static get _ForeachStart() {
                return /@{foreach x in (.*)}/;
        }

        static get _IfStart() {
                return /@{if\((.*)\)}/;
        }

        static get _IfEnd() {
                return '@{/if}';
        }

        static get _ExecStart() {
                return '@{exec}';
        }

        static get _ExecEnd() {
                return '@{/exec}';
        }

        static get _ForStart() {
                return /@{for\((.*)\)}/;
        }

        static get _ForEnd() {
                return '@{/for}';
        }

        static get Template() {
                throw 'must override Template';
        }

        static Render(model, html = this.Template, x = null) {
                let exprArr;

                while ((exprArr = /@{.*?}/.exec(html)) !== null) {
                        let exprRaw = exprArr[0];
                        let expr = exprRaw.replace(/(^@{)|(}$)/g, '');

                        // code is repeating itself a lot QQ
                        // todo: refactor (or fucking use handlebars for reals)
                        // handle FOREACHs
                        if (TemplateEngine._ForeachStart.test(exprRaw)) {
                                let foreachExpArr = TemplateEngine._ForeachStart.exec(exprRaw);

                                let endIdx = html.indexOf(TemplateEngine._ForeachEnd, foreachExpArr.index);

                                let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForeachEnd.length);
                                let template = templateRaw.substr(foreachExpArr[0].length,
                                        templateRaw.length - TemplateEngine._ForeachEnd.length - foreachExpArr[0].length);

                                let expressedTemplate = TemplateEngine._ApplyForeach(template, model, foreachExpArr[1]);

                                html = html.replace(templateRaw, expressedTemplate);
                        }
                        // handle IFs
                        else if (TemplateEngine._IfStart.test(exprRaw)) {
                                let ifExprArr = TemplateEngine._IfStart.exec(exprRaw);

                                let endIdx = html.indexOf(TemplateEngine._IfEnd, ifExprArr.index);

                                let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._IfEnd.length);
                                let template = templateRaw.substr(ifExprArr[0].length,
                                        templateRaw.length - TemplateEngine._IfEnd.length - ifExprArr[0].length);

                                let expressedTemplate = TemplateEngine._ApplyIf(template, model, ifExprArr[1]);

                                html = html.replace(templateRaw, expressedTemplate);
                        }
                        // handle exec
                        else if (exprRaw === TemplateEngine._ExecStart) {
                                let endIdx = html.indexOf(TemplateEngine._ExecEnd, exprArr.index);

                                let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ExecEnd.length);
                                let template = templateRaw.substr(TemplateEngine._ExecStart.length,
                                        templateRaw.length - TemplateEngine._ExecEnd.length - TemplateEngine._ExecStart.length);

                                let expressedTemplate = TemplateEngine._ApplyExec(template, model);

                                html = html.replace(templateRaw, expressedTemplate);
                        }
                        // handle FORs
                        else if (TemplateEngine._ForStart.test(exprRaw)) {
                                let forExprArr = TemplateEngine._ForStart.exec(exprRaw);

                                let endIdx = html.indexOf(TemplateEngine._ForEnd, forExprArr.index);

                                let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForEnd.length);
                                let template = templateRaw.substr(forExprArr[0].length,
                                        templateRaw.length - TemplateEngine._ForEnd.length - forExprArr[0].length);

                                let expressedTemplate = TemplateEngine._ApplyFor(template, model, x, forExprArr[1]);

                                html = html.replace(templateRaw, expressedTemplate);
                        }
                        // handle the stuff
                        else {
                                let expressed;
                                try {
                                        expressed = eval(expr);
                                }
                                catch (ex) {
                                        expressed = ex;
                                }
                                html = html.replace(exprRaw, expressed);
                        }
                }

                return html;
        }

        // does not support attributes with spaces in them, huehuehue
        static RenderElement(model, html = this.Template) {
                html = TemplateEngine.Render(model, html);

                // it's a Kirby!
                let rootElTagRes = /<(.*)>/.exec(html);

                let split = rootElTagRes[1].split(' ');

                // strip out parent el tags, replace replace replace REPLACE
                html = html.replace(rootElTagRes[0], '').replace(rootElTagRes[0].replace('<', '</'), '');

                let element = document.createElement(split[0]);

                // set attributes
                for (let i = 1; i < split.length; i++) {
                        let attr = split[i].split('=');
                        if (attr.length === 2)
                                element.setAttribute(attr[0], attr[1].replace(/"/g, ''));
                }

                element.innerHTML = html;

                return element;
        }

        // does not handle foreach inside foreach, huehuehue
        // also foreach x syntax is set in stone, cannot override x, huehuehue
        static _ApplyForeach(template, model, forEachStr) {
                let html = '';

                eval(forEachStr).forEach((x, i) => {
                        if (typeof x === 'object') x._INDEX = i;
                        html += TemplateEngine.Render(null, template, x);
                });

                return html;
        }

        // does not handle if inside if, might work with foreach though?
        // else is too hard, huehuehuehue (and saturation)
        static _ApplyIf(template, model, conditionStr) {
                if (!eval(conditionStr)) return '';

                return TemplateEngine.Render(model, template);
        }

        static _ApplyExec(execStr, model) {
                eval(execStr);

                return '';
        }

        static _ApplyFor(template, model, x, expr) {
                let html = '';

                // I'll admit - JavaScript is quite fun lawl
                expr = `for(${expr}) { html += TemplateEngine.Render(model, template, x); }`;
                eval(expr);

                return html;
        }
}

//--- templates/loaderTemplate.js
class LoaderTemplate extends TemplateEngine {

        static _loadStyle() {
                if (LoaderTemplate._styleLoaded) return;

                LoaderTemplate._styleLoaded = true;
                document.head.innerHTML += `<style>
.spinner {
  margin: 100px auto;
  width: 40px;
  height: 40px;
  position: relative;
}

.cube1, .cube2 {
  background-color: royalblue;
  width: 15px;
  height: 15px;
  position: absolute;
  top: 0;
  left: 0;

  -webkit-animation: sk-cubemove 1.8s infinite ease-in-out;
  animation: sk-cubemove 1.8s infinite ease-in-out;
}

.cube2 {
  -webkit-animation-delay: -0.9s;
  animation-delay: -0.9s;
}

@-webkit-keyframes sk-cubemove {
  25% { -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5) }
  50% { -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg) }
  75% { -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5) }
  100% { -webkit-transform: rotate(-360deg) }
}

@keyframes sk-cubemove {
  25% {
    transform: translateX(42px) rotate(-90deg) scale(0.5);
    -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5);
  } 50% {
    transform: translateX(42px) translateY(42px) rotate(-179deg);
    -webkit-transform: translateX(42px) translateY(42px) rotate(-179deg);
  } 50.1% {
    transform: translateX(42px) translateY(42px) rotate(-180deg);
    -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg);
  } 75% {
    transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5);
    -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5);
  } 100% {
    transform: rotate(-360deg);
    -webkit-transform: rotate(-360deg);
  }
}
</style>`;
        }

        // from https://github.com/tobiasahlin/SpinKit
        static get Template() {
                LoaderTemplate._loadStyle();
                return `
<div class="spinner">
  <div class="cube1"></div>
  <div class="cube2"></div>
</div>
`;
        }
}
LoaderTemplate._styleLoaded = false; // wow javascript, no other way to use static fields, good job!

//--- templates/lastPostsTemplate.js
class LastPostsTemplate extends TemplateEngine {
        static get Template() {
                return `
<h1>Paskutiniai pranešimai</h1>
<table border="1" cellspacing="0" cellpadding="5" id="last-posts">
        <tbody>
        <tr class="colhead">
                <td class="tcenter">Autorius</td>
                <td class="tcenter">Žinutė</td>
                <td class="tcenter">Laikas</td>
        </tr>
                @{foreach x in model}
                <tr>
                        <td><a href="userdetails.php?id=@{x.UserId}">@{x.Username}</a></td>
                        <td class="hover last-post-content" data-post-id="@{x.Id}" data-thread-id="@{x.ThreadId}">
                                @{x.Content}
                        </td>
                        <td class="tcenter" style="min-width: 70px;">@{Utils.PrintDate(x.Date)}</td>
                </tr>
                @{/foreach}
        </tbody>
</table>

<style>
        .last-post-content {
                cursor: pointer;
        }
</style>
`;
        }
}

//--- templates/topUserTableTemplate.js
class TopUserTableTemplate extends TemplateEngine {
        static get Template() {
                return `
<table class="top-table">
        <tbody>
        <tr class="colhead">
                <td class="tcenter">#</td>
                <td class="tcenter">Vartotojas</td>
                <td class="tcenter">Žinutės</td>
        </tr>
        </tbody>
                @{foreach x in model}
                <tr>
                        <td class="tright">@{x._INDEX + 1}</td>
                        <td><a href="/userdetails.php?id=@{x.Id}">@{x.Username}</a></td>
                        <td class="tright">@{x.PostCount}</td>
                </tr>
                @{/foreach}
</table>`;
        }
}

//--- templates/topPostersTemplate.js
class TopPostersTemplate extends TemplateEngine {
        static get Template() {
                return `
<div id="top-container">
        <div class="top-item">
                <h2>All time</h2>
                @{TopUserTableTemplate.Render(model.All)}
        </div>

        <div class="top-item">
                <h2>Paskutiniai metai</h2>
                @{TopUserTableTemplate.Render(model.Year)}
        </div>

        <div class="top-item">
                <h2>Paskutinis mėnesis</h2>
                @{TopUserTableTemplate.Render(model.Month)}
        </div>
</div>

<style>
        #top-container {
                display: flex;
                justify-content: center;
        }

        .top-item {
                width: auto;
                padding: 20px;
        }

        .top-table td {
                padding-top: 7px;
                padding-bottom: 7px;
                padding-left: 6px;
                padding-right: 6px;
        }
</style>
`;
        }
}

//--- templates/userInfoTemplate.js
class UserInfoTemplate extends TemplateEngine {
        static get Template() {
                return `
<tr>
        <td class="rowhead">Žinutės:</td>
        <td align="left">
                ~ <a href="/userhistory.php?action=viewposts&id=@{model.Id}">@{model.PostCount}</a>
        </td>
</tr>

<tr>
        <td class="rowhead">Paskutinė:</td>
        <td align="left">@{Utils.PrintDate(model.LastPostDate)}</td>
</tr>
`;
        }

        // override, returns array with two tr elements
        static RenderElement(model) {
                let trs = [];

                let splitTemplate = this.Template.split('\n\n');

                trs.push(super.RenderElement(model, splitTemplate[0]));
                trs.push(super.RenderElement(model, splitTemplate[1]));

                return trs;
        }
}

//--- templates/userPostsTemplate.js
class UserPostsTemplate extends TemplateEngine {
        static get Template() {
                return `
<h1>
        <a href="/userdetails.php?id=@{model.UserId}"><b>@{model.Username}</b></a> postų istorija
</h1>
@{UserPostsPaginationTemplate.Render(model)}

<table class="main" border="0" cellspacing="0" cellpadding="0">
        <tbody>
        <tr>
                <td class="embedded">
                        <table width="98%" border="1" cellspacing="0" cellpadding="10">
                                <tbody>
                                <tr>
                                        <td>
                                                @{foreach x in model.Posts}
                                                        <p class="sub"></p>
                                                        <table border="0" cellspacing="0" cellpadding="0">
                                                                <tbody>
                                                                <tr>
                                                                        <td class="embedded">
                                                                               @{Utils.PrintDate(x.Date)} -- <b>Tema:</b> <a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}">@{x.ThreadTitle}</a>
                                                                               -- <b>Posto ID: </b>#<a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}&page=p@{x.Id}#@{x.Id}">@{x.Id}</a>
                                                                        </td>
                                                                </tr>
                                                                </tbody>
                                                        </table>
                                                        <p></p>
                                                        <table class="main" width="97%" border="1" cellspacing="0" cellpadding="5">
                                                                <tbody>
                                                                <tr valign="top">
                                                                        <td class="comment">
                                                                               @{x.Content}
                                                                        </td>
                                                                </tr>
                                                                </tbody>
                                                        </table>
                                                @{/foreach}
                                        </td>
                                </tr>
                                </tbody>
                        </table>
                </td>
        </tr>
        </tbody>
</table>

@{UserPostsPaginationTemplate.Render(model)}
`;
        }
}

//--- templates/userPostsPaginationTemplate.js
class UserPostsPaginationTemplate extends TemplateEngine {
        // fuck me
        static get Template() {
                return `
@{exec}
model.totalPages = Math.ceil(model.TotalPosts / 25);
model.pagination = [];
if (model.Page > model.totalPages) model.Page = model.totalPages;

for (let i = 0; i < model.totalPages; i++) {
        let lowerBound = i * 25 + 1;
        var upperBound = Math.min(lowerBound + 24, model.TotalPosts);
        model.pagination[i] = [lowerBound, upperBound];
}

model.j = 0;
@{/exec}
<p align="center">
<!-- prev and first sticky -->
@{if(model.Page === 1)}
        <span class="pageinactive">«&nbsp;Ankstesnis</span>
        <span class="pageinactive">1&nbsp;-&nbsp;@{model.pagination[0][1]}</span>
@{/if}
@{if(model.Page !== 1)}
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page - 1}">«&nbsp;Ankstesnis</a>
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=1">1&nbsp;-&nbsp;@{model.pagination[0][1]}</a>
@{/if}
<!-- render dots if needed -->
@{if(model.Page > 4)}
 ...
@{/if}
<!-- render 2 previous buttons -->
@{for(model.j = Math.max(model.Page - 3, 1); model.j < model.Page - 1; model.j++)}
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</a>
@{/for}
<!-- render current button -->
@{exec}
if (model.Page === 1) model.j = 0;
@{/exec}
@{if(model.j !== 0 && model.j !== model.totalPages - 1)}
        <span class="pageinactive">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</span>
@{/if}
<!-- render 2 next buttons -->
@{for(model.j = model.j + 1; model.j < Math.min(model.totalPages - 1, model.Page + 2); model.j++)}
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</a>
@{/for}
<!-- dooooots -->
@{if(model.Page < model.totalPages - 3)}
 ...
@{/if}
<!-- next and last sticky -->
@{if(model.Page === model.totalPages && model.totalPages !== 1)}
        <span class="pageinactive">@{model.pagination[model.totalPages-1][0]}&nbsp;-&nbsp;@{model.pagination[model.totalPages - 1][1]}</span>
@{/if}
@{if(model.Page === model.totalPages)}
        <span class="pageinactive">Kitas&nbsp;»</span>
@{/if}
@{if(model.Page !== model.totalPages && model.totalPages !== 1)}
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.totalPages}">@{model.pagination[model.totalPages - 1][0]}&nbsp;-&nbsp;@{model.pagination[model.totalPages - 1][1]}</a>
@{/if}
@{if(model.Page !== model.totalPages)}
        <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page + 1}">Kitas&nbsp;»</a>
@{/if}
</p>
`;
        }
}

//--- templates/showOriginalPostTemplate.js
class ShowOriginalPostTemplate extends TemplateEngine {
        static get Template() {
                return `<p class="small">
<span style="cursor: pointer;">Rodyti originalų pranešimą?</span>
</p>`;
        }

        static get TemplateOriginalPost() {
                return `
<p class="sub">
   Originalus postas:
</p>
<table class="main" border="1" cellspacing="0" cellpadding="10">
   <tbody>
      <tr>
         <td style="border: 1px black dotted">@{model.Content}</td>
      </tr>
   </tbody>
</table>`;
        }

        static RenderOriginalPost(model) {
                return this.Render(model, this.TemplateOriginalPost);
        }
}

//--- templates/userSettingsTemplate.js
class UserSettingsTemplate extends TemplateEngine {
        static get HeaderTemplate() {
                return `<h1>
        BetterLM nustatymai
</h1>`;
        }

        static RenderHeaderElement() {
                return this.RenderElement(null, this.HeaderTemplate);
        }

        static get Template() {
                return `
<table border="1" cellspacing="0" cellpadding="10" align="center" width="100%">
<!-- I don't even this html structure -->
<tbody><tr><td colspan="7"><table border="1" cellspacing="0" cellpadding="5" width="100%"><tbody>

<tr>
        <td class="rowhead" valign="top" align="right">Rodyti paskutinius pranešimus</td>
        <td valign="top" align="left">
                <label><input type="checkbox" id="showLastPosts">Rodyti paskutinius pranešimus</label>
        </td>
</tr>

<tr>
        <td class="rowhead" valign="top" align="right">Ignoravimo nustatymai</td>
        <td valign="top" align="left">
                <label><input type="checkbox" id="hideTopics">Slėpti sukurtas temas forume</label><br>
                <label><input type="checkbox" id="replaceMessages">Pakeisti žinutės tekstą vietoje pašalinimo:</label><br>
                <input type="text" id="replaceString">
        </td>
</tr>

<tr>
        <td class="rowhead" valign="top" align="right">Ignoruotieji</td>
        <td valign="top" align="left">
                @{if(model.ignored.length > 0)}
                <select size="@{Math.min(12, model.ignored.length)}" multiple id="ignoredList">
                        @{foreach x in model.ignored}
                        <option value="@{x}">@{x}</option>
                        @{/foreach}
                </select><br/>
                <button id="blmRemoveIgnoredBtn" type="button">Pašalinti</button>
                @{/if}
                @{if(model.ignored.length <= 0)}
                <div>Nieko neignoruoji, sugyveni draugiškai</div>
                @{/if}
        </td>
</tr>

<tr>
        <td colspan="2" class="center">
                <input type="button" id="blmSaveBtn" value="Patvirtinti!!1" style="height: 25px">
        </td>
</tr>

</tbody></table></td></tr></tbody>
</table>
`;
        }
}

//--- templates/userMentionTemplate.js

//--- modules/userIgnore.js
class UserIgnore extends Base {
        static get _QuoteRegex() {
                return /\[quote=\w*\]/g;
        }

        static get _AuthorRegex() {
                return /(?:\[quote=)(\w*)(?:\])/;
        }

        static get _EndQuoteRegex() {
                return /\[\/quote\]/g;
        }

        init() {
        }

        hideTopics() {
                if (!this.settings.options.ignoreHideTopics) return;

                let tables = document.querySelectorAll('table');
                if (tables.length < 1) return;

                let rows = tables[0].querySelectorAll('tr:not(.colhead)');
                [...rows].forEach(row => {
                        let authorLink = row.querySelector('td:nth-child(4) > a');
                        if (authorLink) {
                                let author = authorLink.textContent;
                                if (author && this.settings.isIgnored(author))
                                        row.remove();
                        }
                });
        }

        clearTorrentDetails() {
                let comments = document.querySelectorAll('#comments > div.comment');
                if (!comments) return;

                [...comments].forEach(comment => {
                        let authorEl = comments.querySelector('div.comment-user > a');
                        if (!authorEl) return;

                        let author = authorEl.textContent;
                        if (!author || !this.settings.isIgnored(author)) return;

                        if (this.settings.options.ignoreReplaceMessages) {
                                let commentText = comment.querySelector('div.comment-text');
                                if (commentText)
                                        commentText.textContent = this.settings.options.ignoreReplaceString;
                        } else {
                                comment.remove();
                        }
                });
        }

        clearReplyQuote() {
                this.clearReply();

                let textArea = document.querySelector('textarea#body');
                if (!textArea) return;

                let text = textArea.value;
                let quotes = text.match(UserIgnore._QuoteRegex);
                if (!quotes) return;

                for (let i = 0; i < quotes.length; i++) {
                        let quote = quotes[i];
                        let authorMatch = quote.match(UserIgnore._AuthorRegex);
                        if (!authorMatch || authorMatch.length !== 2) continue;

                        let author = authorMatch[1];
                        if (this.settings.isIgnored(author)) {
                                try {
                                        let idxStart = text.indexOf(quote) + quote.length;
                                        if (idxStart === (-1 + quote.length)) throw 'mangled markup';

                                        let idxEnd;

                                        /* [quote] and [/quote] count should equal, otherwise formatting
                                         is mangled and there's fuck I can do and the fuck I care lel */
                                        for (let j = 0; j < quotes.length; j++) {
                                                let match = UserIgnore._EndQuoteRegex.exec(text);
                                                // black box logic go figure
                                                if (quotes.length - 1 - j !== i) continue;
                                                idxEnd = match.index;
                                                break;
                                        }

                                        let toReplace = text.substring(idxStart, idxEnd);
                                        text = text.replace(toReplace, this.settings.options.ignoreReplaceString);
                                        textArea.value = text;
                                        return;
                                }
                                catch (ex) { /* noop */
                                }
                        }
                }
        }

        clearReply() {
                let replies = document.querySelectorAll('p.sub');

                for (let i = 0; i < replies.length; i++) {
                        let reply = replies[i];

                        let authorParts = reply.textContent.split(' ');
                        if (authorParts.length < 2) return;

                        let author = authorParts[1];
                        let contentEl = reply.nextElementSibling;

                        if (this.settings.isIgnored(author)) {
                                if (this.settings.options.ignoreReplaceMessages) {
                                        let textEl = contentEl.querySelector('tbody > tr > td:last-child');
                                        if (textEl)
                                                textEl.textContent = this.settings.options.ignoreReplaceString;
                                } else {
                                        contentEl.remove();
                                        reply.remove();
                                }
                        } else {
                                this.clearQuote(contentEl);
                        }
                }
        }

        clearQuote(el) {
                try {
                        let quoteHeaders = el.querySelectorAll('p.sub');

                        for (let j = 0; j < quoteHeaders.length; j++) {
                                let quoteHeader = quoteHeaders[j];
                                let quoteAuthorArr = quoteHeader.textContent.split(' ');
                                if (!quoteAuthorArr || quoteAuthorArr.length !== 2)    continue;

                                let quoteAuthor = quoteAuthorArr[0];
                                if (this.settings.isIgnored(quoteAuthor)) {
                                        let quoteContent = quoteHeader.nextElementSibling.querySelector('td');
                                        quoteContent.innerHTML = this.settings.options.ignoreReplaceString;
                                }
                        }
                }
                catch (ex) { /* do not expect a comment in every empty catch block */
                }
        }

        clearTopic() {
                let posts = Utils.GetPosts();
                this._clearTopic();

                // todo: move to separate function, come on...
                for (let i = 0; i < posts.length; i++) {
                        let post = posts[i];
                        let authorLink = post.querySelector('p > span > a');
                        if (!authorLink || !authorLink.href) continue;

                        let author = authorLink.textContent;
                        if (author === this.settings.ownName || !author) continue;

                        let ignored = this.settings.isIgnored(author);

                        // render ignore/unignore button
                        let link = document.createElement('a');
                        link.textContent = ignored ? 'Nebeignoruoti' : 'Ignoruoti';
                        link.dataset.author = author;
                        link.dataset.ignored = ignored;
                        link.href = '#';
                        link.onclick = evt => {
                                let link = evt.target;
                                let author = link.dataset.author;
                                let ignored = link.dataset.ignored;

                                if (ignored === 'false') {
                                        if (confirm(`Ar tikrai norite ignoruoti ${author}?`)) {
                                                this.settings.addIgnored(author);
                                                location.reload(); // too much to change
                                        }
                                } else {
                                        if (confirm(`Ar tikrai nebenorite ignoruoti ${author}?`)) {
                                                this.settings.removeIgnored(author, true);
                                                location.reload();
                                        }
                                }

                                evt.preventDefault();
                        };

                        let span = post.querySelector('p > span');

                        let el = Utils.InsertAfter(span, document.createTextNode('['));
                        el = Utils.InsertAfter(el, link);
                        Utils.InsertAfter(el, document.createTextNode('] '));
                }
        }

        _clearTopic() {
                let posts = Utils.GetPosts();

                for (let i = 0; i < posts.length; i++) {
                        let post = posts[i];
                        let authorLink = post.querySelector('p > span > a');

                        if (authorLink) {
                                let author = authorLink.textContent;
                                let content = post.querySelector('td.forumpost[id^="post_"]');

                                // re-retardify posts
                                if (author && this.settings.isIgnored(author)) {
                                        if (this.settings.options.ignoreReplaceMessages) {
                                                content.innerHTML = this.settings.options.ignoreReplaceString;
                                                let signatureEl = post.querySelector('p.sig');
                                                if (signatureEl)
                                                        signatureEl.remove();
                                        } else
                                                post.remove();
                                }
                        }
                }
        }

        renderUserDetails() {
                let authorEl = document.querySelector('td.embedded > h1');
                if (!authorEl) return;

                let author = authorEl.innerText;
                let ignored = this.settings.isIgnored(author);
                let blockEl = document.querySelector('a[href^="friends.php?action=add&type=block&"]');
                let insertEl = document.createElement('a');

                insertEl.innerText = ignored ? 'pašalinti iš ignoravimo' : 'ignoruoti';
                insertEl.href = '#';

                let inserted = Utils.InsertAfter(blockEl.nextSibling, document.createTextNode(' - ('));
                Utils.InsertAfter(inserted, insertEl);
                Utils.InsertAfter(insertEl, document.createTextNode(')'));

                insertEl.onclick = () => {
                        if (ignored)
                                this.settings.removeIgnored(author, true);
                        else
                                this.settings.addIgnored(author);
                        location.reload();
                };
        }
}

//--- modules/lastPosts.js
class LastPosts extends Base {
        async init() {
                if (!this.settings.options.showLastPosts) return Promise.resolve();

                let bottomEl = document.querySelector('p.tcenter:last-child');
                let lastPostsEl = document.createElement('p');
                lastPostsEl.innerHTML = LoaderTemplate.Render();

                Utils.InsertAfter(bottomEl, lastPostsEl);

                let posts = await Utils.FetchJSON('/forums/lastposts', 'POST', this.settings.getIgnored());
                lastPostsEl.innerHTML = LastPostsTemplate.Render(posts);

                // set width (fuck css, this hack is awesome)
                let forumTable = document.querySelector('#forum');
                let lastPostTable = document.querySelector('#last-posts');
                lastPostTable.width = forumTable.offsetWidth;

                let contentLinks = lastPostTable.querySelectorAll('td[data-post-id]');

                for (let i = 0; i < contentLinks.length; i++) {
                        let postId = contentLinks[i].getAttribute('data-post-id');
                        let threadId = contentLinks[i].getAttribute('data-thread-id');

                        contentLinks[i].addEventListener('click', () => {
                                location.href = `/forums.php?action=viewtopic&topicid=${threadId}&page=p${postId}#${postId}`;
                        });
                }
        }
}

//--- modules/topPosters.js
class TopPosters {
        async renderTable() {
                document.querySelector('#content > table.main').remove();
                let contentEl = document.querySelector('#content');
                contentEl.innerHTML = LoaderTemplate.Render();

                let top = await Utils.FetchJSON('/forums/top');

                contentEl.innerHTML = TopPostersTemplate.Render(top);
        }

        renderTopLinks() {
                let searchAnchors = document.querySelectorAll('a[href="?action=search"]');

                [...searchAnchors].forEach(searchAnchor => {
                        let anchor = document.createElement('a');
                        anchor.href = '/forums.php?action=top';
                        anchor.innerText = 'Top';

                        Utils.InsertBefore(searchAnchor, anchor);
                        Utils.InsertAfter(anchor, document.createTextNode(' | '));
                });
        }
}

//--- modules/userInfo.js
class UserInfo {
        constructor(userId) {
                this._userId = userId;
        }

        async renderPostCount() {
                let userInfo = await Utils.FetchJSON(`/users/info/${this._userId}`);
                if (!userInfo) return;

                let rowToAppendAfter = document.querySelector('table.main tr:nth-child(4)');

                let rows = UserInfoTemplate.RenderElement(userInfo);

                rowToAppendAfter = Utils.InsertAfter(rowToAppendAfter, rows[0]);
                Utils.InsertAfter(rowToAppendAfter, rows[1]);
        }
}

//--- modules/userPosts.js
class UserPosts {
        constructor(userId, page) {
                this._userId = userId;
                this._page = page;
        }

        async init() {
                document.querySelector('#content > table.main').remove();
                let contentEl = document.querySelector('#content');
                contentEl.innerHTML = LoaderTemplate.Render();

                let userPosts = await Utils.FetchJSON(`/users/${this._userId}/posts/${this._page > 1 ? this._page : ''}`);

                contentEl.innerHTML = UserPostsTemplate.Render(userPosts);
        }
}

//--- modules/originalPost.js
class OriginalPost {
        constructor() {
                this.dateSince = new Date('2017-05-21');
        }

        init() {
                Utils.GetPosts().forEach(post => {
                        if (Utils.GetPostDate(post) < this.dateSince) return;

                        let postEditedEl = post.querySelector('.forumpost p.small');

                        if (postEditedEl && postEditedEl.textContent.startsWith('Paskutinį kartą redagavo:')) {
                                // render 'show original button'
                                let postId = Utils.GetPostId(post);

                                let showOriginalEl = ShowOriginalPostTemplate.RenderElement(postId);
                                Utils.InsertAfter(postEditedEl, showOriginalEl);

                                showOriginalEl.onclick = async () => {
                                        showOriginalEl.onclick = null;

                                        // too fast to show loader
                                        // showOriginalEl.innerHTML = LoaderTemplate.Render();

                                        let origPost = await Utils.FetchJSON(`/forums/post/${postId}`);
                                        showOriginalEl.innerHTML = ShowOriginalPostTemplate.RenderOriginalPost(origPost);
                                };
                        }
                });
        }
}

//--- modules/userSettings.js
class UserSettings extends Base {
        init() {
                let self = this;
                let contentEl = document.querySelector('#content');

                let headerEl = UserSettingsTemplate.RenderHeaderElement();
                let settingsEl = UserSettingsTemplate.RenderElement(this.settings);

                Utils.InsertAfter(contentEl.querySelector('table'), headerEl);
                Utils.InsertAfter(headerEl, settingsEl);

                // button remove ignored
                let btnRemoveIgnored = settingsEl.querySelector('#blmRemoveIgnoredBtn');
                if (btnRemoveIgnored)
                        btnRemoveIgnored.onclick = () => {
                                let healedPlebs = Utils.GetSelectedValues(settingsEl.querySelector('#ignoredList'));

                                healedPlebs.forEach(pleb => this.settings.removeIgnored(pleb, false));

                                let removeOptions = settingsEl.querySelectorAll('#ignoredList > option:checked');
                                removeOptions.forEach(opt => opt.remove());
                        };

                // button save settings
                let btnSave = settingsEl.querySelector('#blmSaveBtn');
                btnSave.onclick = () => {
                        this.settings.save();
                        location.reload();
                };

                // checkbox last posts
                let lastPostsEl = settingsEl.querySelector('#showLastPosts');
                lastPostsEl.checked = self.settings.options.showLastPosts;
                lastPostsEl.onchange = () => self.settings.options.showLastPosts = lastPostsEl.checked;

                // checkbox hide topics
                let hideTopicEl = settingsEl.querySelector('#hideTopics');
                hideTopicEl.checked = self.settings.options.ignoreHideTopics;
                hideTopicEl.onchange = () => self.settings.options.ignoreHideTopics = hideTopicEl.checked;

                // checkbox replace messages
                let replaceMessagesEl = settingsEl.querySelector('#replaceMessages');
                replaceMessagesEl.checked = self.settings.options.ignoreReplaceMessages;
                replaceMessagesEl.onchange = () => self.settings.options.ignoreReplaceMessages = replaceMessagesEl.checked;

                // input replace string
                let replaceStringEl = settingsEl.querySelector('#replaceString');
                replaceStringEl.value = self.settings.options.ignoreReplaceString;
                replaceStringEl.onchange = () => self.settings.options.ignoreReplaceString = replaceStringEl.value;
        }
}

//--- modules/userMention.js
// class UserMention {
//      constructor(textArea) {
//              this._textArea = textArea;
//              textArea.oninput = evt => {
//                      let entered = textArea.value[textArea.selectionStart - 1];
//                      if (entered === '@') {
//                              let prevSymbol = textArea.value[textArea.selectionStart - 2];
//                              // check for conditions to display autocomplete
//                              if (prevSymbol === undefined || prevSymbol === ' ' || prevSymbol === '\n'
//                                      || prevSymbol === ']' || prevSymbol === ':' || prevSymbol === '>') {
//                              }
//                      }
//              };
//      }
// }

//--- modules/deletedUsernames.js
class DeletedUsernames {
        async init() {
                let anchorIdMap = new Map();

                let posts = Utils.GetPosts();

                [...posts].forEach(posts => {
                        let anchor = posts.querySelector('p > span > a');
                        if (anchor && anchor.text === '') {
                                let id = Number(anchor.href.split('?id=')[1]);

                                if (anchorIdMap.has(id))
                                        anchorIdMap.get(id).push(anchor);
                                else
                                        anchorIdMap.set(id, [anchor]);
                        }
                });

                if (anchorIdMap.size === 0) return;

                let usernames = await Utils.FetchJSON('/users/usernames', 'POST', [...anchorIdMap.keys()]);

                if (!usernames) return; // yeah time to start defensive programming in case service is unreachable?

                usernames.forEach(u => {
                        let anchors = anchorIdMap.get(u.Id);

                        anchors.forEach(a => {
                                a.text = `~ ${u.Username}`;
                                a.removeAttribute('href');
                        });
                });
        }
}

//--- modules/twitchEmotes.js
class TwitchEmotes extends Base {
        init() {
                // intercept editMessageSubmit function
                let origSubmitFn = window.editMessageSubmit;
                if (origSubmitFn) {
                        window.editMessageSubmit = (form, id) => {
                                this.handleSubmit(form);
                                origSubmitFn.apply(this, [form, id]);
                        };
                }

                document.addEventListener('submit', evt => this.handleSubmit(evt.target));
        }

        handleSubmit(form) {
                let textArea = form.querySelector('textarea');

                textArea.value = textArea.value.replace(TwitchEmotes._EmotesRegex, (match, s1, s2, s3) => `${s1}${this.formImgEl(s2)}${s3}`);
        }

        formEmoteUrl(emote) {
                return `${Settings.ApiUrl}/assets/images/twitch/${emote}.png`;
        }

        formImgEl(emote) {
                return `[img]${this.formEmoteUrl(emote)}[/img]`;
        }

        // generated by tools/RipTwitchEmotes
        static get _EmotesRegex() {
                return new RegExp(`(\\.|\\n|^|,| |$)(4Head|AMPTropPunch|ANELE|ArgieB8|ArigatoNas|ArsonNoSexy|AsianGlow|BabyRage|BatChest|BCWarrior|BegWan|BibleThump|BigBrother|BigPhish|BJBlazkowicz|BlargNaut|bleedPurple|BlessRNG|BloodTrail|BrainSlug|BrokeBack|BuddhaBar|BudStar|CarlSmile|ChefFrank|cmonBruh|CoolCat|CoolStoryBob|copyThis|CorgiDerp|CrreamAwk|CurseLit|DAESuppy|DansGame|DatSheffy|DBstyle|DendiFace|DogFace|DoritosChip|duDudu|DxCat|EagleEye|EleGiggle|FailFish|FrankerZ|FreakinStinkin|FUNgineer|FunRun|FutureMan|GingerPower|GivePLZ|GOWSkull|GrammarKing|HassaanChop|HassanChop|HeyGuys|HotPokket|HumbleLife|imGlitch|InuyoFace|ItsBoshyTime|Jebaited|JKanStyle|JonCarnage|KAPOW|Kappa|KappaClaus|KappaPride|KappaRoss|KappaWealth|Kappu|Keepo|KevinTurtle|Kippa|KonCha|Kreygasm|Mau5|mcaT|MikeHogu|MingLee|MorphinTime|MrDestructoid|MVGame|NinjaGrumpy|NomNom|NotATK|NotLikeThis|OhMyDog|OneHand|OpieOP|OptimizePrime|OSblob|OSfrog|OSkomodo|OSsloth|panicBasket|PanicVis|PartyTime|pastaThat|PeoplesChamp|PermaSmug|PicoMause|PipeHype|PJSalt|PJSugar|PMSTwin|PogChamp|Poooound|PraiseIt|PRChase|PrimeMe|PunchTrees|PunOko|QuadDamage|RaccAttack|RalpherZ|RedCoat|ResidentSleeper|riPepperonis|RitzMitz|RlyTho|RuleFive|SabaPing|SeemsGood|ShadyLulu|ShazBotstix|SmoocherZ|SMOrc|SoBayed|SoonerLater|SPKFace|SPKWave|Squid1|Squid2|Squid3|Squid4|SSSsss|StinkyCheese|StoneLightning|StrawBeary|SuperVinlin|SwiftRage|TakeNRG|TBAngel|TBCrunchy|TBTacoBag|TBTacoProps|TearGlove|TehePelo|TF2John|ThankEgg|TheIlluminati|TheRinger|TheTarFu|TheThing|ThunBeast|TinyFace|TooSpicy|TriHard|TTours|TwitchLit|twitchRaid|TwitchRPG|TwitchUnity|UncleNox|UnSane|UWot|VaultBoy|VoHiYo|VoteNay|VoteYea|WholeWheat|WTRuck|WutFace|YouDontSay|YouWHY)(\\b)`, 'g');
        }
}

//--- pages/forumPage.js
class ForumPage {
        init() {
                let lastPosts = new LastPosts();
                lastPosts.init();

                let topPosters = new TopPosters();
                topPosters.renderTopLinks();
        }
}

//--- pages/forumViewPage.js
class ForumViewPage {
        init() {
                // let forumId = Number(location.href.match(/forumid=(\d+)/)[1]);
                //
                let userIgnore = new UserIgnore();
                userIgnore.hideTopics();
        }
}

//--- pages/profilePage.js
class ProfilePage {
        init() {
                let userSettings = new UserSettings();
                userSettings.init();
        }
}

//--- pages/replyPage.js
class ReplyPage {
        init() {
                // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
                // let userMention = new UserMention(replyTextArea);
                //
                let userIgnore = new UserIgnore();
                userIgnore.clearReply();

                let twitchEmotes = new TwitchEmotes();
                twitchEmotes.init();
        }
}

//--- pages/replyQuotePage.js
class ReplyQuotePage {
        init() {
                // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
                // let userMention = new UserMention(replyTextArea);
                //
                let userIgnore = new UserIgnore();
                userIgnore.clearReplyQuote();

                let twitchEmotes = new TwitchEmotes();
                twitchEmotes.init();
        }
}

//--- pages/topPage.js
class TopPage {
        init() {
                let topPosters = new TopPosters();
                topPosters.renderTable();
        }
}

//--- pages/torrentDetailPage.js
class TorrentDetailPage {
        init() {
                let userIgnore = new UserIgnore();
                userIgnore.clearTorrentDetails();
        }
}

//--- pages/userDetailsPage.js
class UserDetailsPage extends Base {
        init() {
                let userDetailsId = Number(location.href.match(new RegExp(/\.php\?id=(\d+)/))[1]);

                if (userDetailsId !== this.settings.ownUserId) {
                        let userInfo = new UserInfo(userDetailsId);
                        userInfo.renderPostCount();

                        let userIgnore = new UserIgnore();
                        userIgnore.renderUserDetails();
                }
        }
}

//--- pages/userHistoryPage.js
class UserHistoryPage extends Base {
        init() {
                let userId = Number(location.href.match(/\/userhistory\.php\?action=viewposts&id=(\d+)/)[1]);

                if (this.settings.ownUserId === userId) return;

                let pageNumber;
                // and right about here I went 'fuck it'
                try {
                        pageNumber = Number(location.href.match(/page=(\d+)/)[1]);
                }
                catch (ex) {
                        pageNumber = 1;
                }

                let userPosts = new UserPosts(userId, pageNumber);
                userPosts.init();
        }
}

//--- pages/viewTopicPage.js
class ViewTopicPage {
        init() {
                // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
                // let userMention = new UserMention(replyTextArea);
                //
                let userIgnore = new UserIgnore();
                userIgnore.clearTopic();

                let originalPost = new OriginalPost();
                originalPost.init();

                let deletedUsernames = new DeletedUsernames();
                deletedUsernames.init(); // yep ignore async we don't care, the train is rolling

                let twitchEmotes = new TwitchEmotes();
                twitchEmotes.init();
        }
}

//--- pages/editPage.js
class EditPage {
        init () {
                // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
                // let userMention = new UserMention(replyTextArea);
                //
                let userIgnore = new UserIgnore();
                userIgnore.clearReply();

                let twitchEmotes = new TwitchEmotes();
                twitchEmotes.init();
        }
}

//--- main.js
class BetterLM {
        static Init() {
                if (BetterLM.IsReply)
                        new ReplyPage().init();

                else if (BetterLM.IsReplyQuote)
                        new ReplyQuotePage().init();

                else if (BetterLM.IsViewTopic)
                        new ViewTopicPage().init();

                else if (BetterLM.IsForum)
                        new ForumPage().init();

                else if (BetterLM.IsForumView)
                        new ForumViewPage().init();

                else if (BetterLM.IsUserDetails)
                        new UserDetailsPage().init();

                else if (BetterLM.IsUserHistory)
                        new UserHistoryPage().init();

                else if (BetterLM.IsTorrentDetail)
                        new TorrentDetailPage().init();

                else if (BetterLM.IsProfile)
                        new ProfilePage().init();

                else if (BetterLM.IsTop)
                        new TopPage().init();

                else if (BetterLM.IsEdit)
                        new EditPage().init();
        }

        static get IsReply() {
                return BetterLM._test(/\/forums\.php\?action=reply&topicid=/);
        }

        static get IsViewTopic() {
                return BetterLM._test(/\/forums\.php\?action=viewtopic/);
        }

        static get IsReplyQuote() {
                return BetterLM._test(/\/forums\.php\?action=quotepost&topicid=/);
        }

        static get IsForum() {
                return BetterLM._test(/\/forums\.php$/);
        }

        static get IsForumView() {
                return BetterLM._test(/\/forums\.php\?action=viewforum/);
        }

        static get IsUserDetails() {
                return BetterLM._test(/\/userdetails\.php\?id=/);
        }

        static get IsUserHistory() {
                return BetterLM._test(/\/userhistory\.php\?action=viewposts&id=/);
        }

        static get IsTorrentDetail() {
                return BetterLM._test(/\/details?/) || BetterLM._test(/\/torrent?/);
        }

        static get IsProfile() {
                return BetterLM._test(/\/my.php(?:(\?edited=1)?)$/);
        }

        static get IsTop() {
                return BetterLM._test(/\/forums\.php\?action=top$/);
        }

        static get IsEdit() {
                return BetterLM._test(/\/forums\.php\?action=editpost/);
        }

        static _test(expr) {
                return expr.test(location.href);
        }
}

// here we go for the brighter tomorrow
BetterLM.Init();