Better Movie-blog

Browse trough the articles with left and right cursor keys

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Better Movie-blog
// @namespace    hoehleg.userscripts.private
// @version      0.5.14
// @description  Browse trough the articles with left and right cursor keys
// @author       Gerrit Höhle
// @match        http://movie-blog.at/
// @match        http://movie-blog.at/page/*
// @match        http://movie-blog.at/category/*
// @match        http://movie-blog.sx/
// @match        http://movie-blog.sx/page/*
// @match        http://movie-blog.sx/category/*
// @match        http://www.movie-blog.org/
// @match        http://www.movie-blog.org/page/*
// @match        http://www.movie-blog.org/category/*
// @match        http://movie-blog.to/
// @match        http://movie-blog.to/page/*
// @match        http://movie-blog.to/category/*
// @match        http://movie-blog.tv/
// @match        http://movie-blog.tv/page/*
// @match        http://movie-blog.tv/category/*
// @match        https://movie-blog.tv/
// @match        https://movie-blog.tv/page/*
// @match        https://movie-blog.tv/category/*
// @require      https://greasyfork.org/scripts/390752-comparator/code/Comparator.js?version=744703
// @require      https://greasyfork.org/scripts/391608-privateproperty/code/PrivateProperty.js?version=744693
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

/* jshint esversion: 6 */
/* globals Comparator, PrivateProperty */

(function() {
    'use strict';
    const CONFIG = {
        // number of pages to load Beitraege to one page
        PAGES_LOAD_COUNT: 2,

        // unit for row "Groesse" in beitraege table
        TABLE_BEITRAEGE_SIZE_DEFAULT_UNIT: "GB",

        // position of sidebar (below image)
        SIDEBAR_POSITION_TOP_PX : 670
    };

    const CONST = (() => {
        const URL_PATH = (() => {
            const path = document.location.pathname.split("/").filter(str => str.length > 0);
            return path === null ? [] : path;
        })();

        const DATA_SIZE_UNITS = (() => {
            const DataSize = function(factor, shortText, longText) {
                this.factor = factor;
                this.shortText = shortText;
                this.longText= longText;
                Object.freeze(this);
            };
            return [
                new DataSize(1<<0, "B", "Byte"),
                new DataSize(1<<10, "KB", "Kilobyte"),
                new DataSize(1<<20, "MB", "Megabyte"),
                new DataSize(1<<30, "GB", "Gigabyte"),
                new DataSize(1<<40, "TB", "Terabyte"),
            ].reduce((map, dataSize) => map.set(dataSize.shortText, dataSize), new Map());
        })();

      const TABLE_BEITRAEGE_HEADER_NAMES = {
            NUM: "#",
            RATING: "IMDb",
            SIZE: "Größe",
            LINK: "Link",
            DATE: "Datum",
            TYPE: "Typ",
            JAHR: "Jahr",
            DURATION: "Dauer"
        };

        const HTML_CLASSES = {
            DATE: "date",
            BEITRAG: "beitrag4",
            TABLE_BEITRAEGE_COLUMN_NUM: "col-num",
            TABLE_BEITRAEGE_COLUMN_RATING: "col-rating",
            TABLE_BEITRAEGE_COLUMN_SIZE: "col-size",
            TABLE_BEITRAEGE_COLUMN_LINK: "col-link",
            TABLE_BEITRAEGE_COLUMN_DATE: "col-date",
            TABLE_BEITRAEGE_COLUMN_TYPE: "col-type",
            TABLE_BEITRAEGE_COLUMN_JAHR: "col-jahr",
            TABLE_BEITRAEGE_COLUMN_DURATION: "col-duration",
            TABLE_ROW_HIGHLIGHTED: "row-highlighted",
            TABLE_COLUMN_SORTABLE: "sortable",
            TABLE_COLUMN_SORTED_ASC: "sorted-asc",
            TABLE_COLUMN_SORTED_DESC: "sorted-desc",
            PREVIOUS: "previous",
            NEXT: "next",
        };

        const HTML_IDS = {
            SHOW_CONTAINER: "show-container",
            BEITRAG_INFO: "beitrag-info",
            PAGE_NUMBER: "page-number",
            BEITRAG_NUMBER: "beitrag-number",
            BEITRAG_COUNT: "beitrag-count",
            FILM_DATA_IMDB_RATING: "film-data-imdb-rating",
            FILM_DATA_GROESSE: "film-data-groesse",
            FILM_DATA_DAUER: "film-data-dauer",
            BUTTON_PAGE_PREV: "btn-page-prev",
            BUTTON_PAGE_NEXT: "btn-page-next",
            BUTTON_BEITRAG_PREV: "btn-beitrag-prev",
            BUTTON_BEITRAG_NEXT: "btn-beitrag-next",
            LISTE_BEITRAEGE: "liste-beitraege",
            HEAD: "head",
            CONTENT: "content",
            TABLE_TR_NUM: "tr-num",
            SIDEBAR: "sidebar",
            CUR_BEITRAG_IMG: "cur-beitrag-img"
        };

        if (URL_PATH && URL_PATH[0] === "category") {
            Object.assign(HTML_CLASSES, {
                DATE: "date_x",
                BEITRAG : "beitrag2"
            });
            Object.assign(HTML_IDS, {
                CONTENT : "archiv"
            });
        }

        // Container
        let CSS = `
			#page {
				background-size: 98%;
				width: 100%;
				height: auto;
			}
			#${HTML_IDS.CONTENT} {
				margin: 0px 0px 0px 0px;
				padding: 0px 0px 0px 3.25%;
				width: 67.5%;
				height: 5000px
			}
			#${HTML_IDS.SIDEBAR} {
				position: absolute;
				top: ${CONFIG.SIDEBAR_POSITION_TOP_PX}px;
				right: 0px;
			}
			#${HTML_IDS.SHOW_CONTAINER} {
				position: relative;
				top: 30px;
			}
  			#${HTML_IDS.LISTE_BEITRAEGE} {
				position: relative;
				width: 100%;
			}
       `;

        // Beitrag
        CSS += `
			#${HTML_IDS.CUR_BEITRAG_IMG} {
				position: fixed;
				top: 2px;
				left: 72%;
				max-width: 400px;
				z-index: 1;
			}
			#${HTML_IDS.BEITRAG_INFO} {
				position: fixed;
				top: 2px;
				left: 80px;
			}
			.${HTML_CLASSES.BEITRAG} {
				position: relative;
				width: 100%;
                margin: 1.3em 0 0 0;
			}
			.${HTML_CLASSES.PREVIOUS}, .${HTML_CLASSES.NEXT} {
                display: inline-block;
                padding: 20;
                margin: 0;
                text-decoration: none;
                text-align: center;
                font-weight: bold;
            }
        `;

        // Table of Beitraege
        CSS += `
            #${HTML_IDS.LISTE_BEITRAEGE} > table {
				width: 100%;
                border: none;
				border-collapse: collapse;
                white-space: nowrap;
                font-size: smaller;
			}
            #${HTML_IDS.LISTE_BEITRAEGE} > table td, #${HTML_IDS.LISTE_BEITRAEGE} > table th {
                border-left: thin solid lightgray;
                border-right: thin solid lightgray;
                border-top: thin solid lightgray;
                padding: 5px 5px;
            }
            #${HTML_IDS.LISTE_BEITRAEGE} > table td:first-child, #${HTML_IDS.LISTE_BEITRAEGE} > table th:first-child {
                border-left: none;
            }

            #${HTML_IDS.LISTE_BEITRAEGE} > table td:last-child, #${HTML_IDS.LISTE_BEITRAEGE} > table th:last-child {
                border-right: none;
            }
           .${HTML_CLASSES.TABLE_ROW_HIGHLIGHTED} {
                background: rgba(255, 240, 200, 0.5);
                padding: 3px 5px;
                margin: -3px -5px;
                line-height: 1.7;
                border-radius: 3px;
           }
           .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_NUM} {
 				width: 1em;
                max-width: 1.5em;
                text-align: right;
            }
            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_RATING} {
 				width: 1em;
                max-width: 3em;
                text-align: center;
            }
            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_JAHR} {
 				width: 1em;
                max-width: 3em;
                text-align: center;
            }
            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK} {
 				width: auto;
 				max-width: 8em;

                text-align: left;

                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK} > a {
                color: black;
                letter-spacing: 1px;
            }
            .${HTML_CLASSES.TABLE_ROW_HIGHLIGHTED} .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK} > a {
                font-weight: 900;
                color: black;
                letter-spacing: 3px;
            }

            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DURATION} {
 				width: 1em;
                max-width: 3.5em;
                text-align: right;
            }
            .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_SIZE} {
                width: 1em;
                max-width: 5em;
                text-align: right;
            }
           .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_TYPE} {
 				width: 1em;
                max-width: 5em;
                text-align: left;
            }
           .${HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DATE} {
 				width: 1em;
                max-width: 9em;
                text-align: left;
                font-size: smaller;
            }
		`;

        // table sort indicators
        CSS += `
            th.${HTML_CLASSES.TABLE_COLUMN_SORTABLE} {
                position: relative;
                cursor: pointer;
            }
            th.${HTML_CLASSES.TABLE_COLUMN_SORTABLE}::before {
                content: "\\21D5\\0020";
                position: relative;
                left: 0px;
                color: #999;
            }
            th.${HTML_CLASSES.TABLE_COLUMN_SORTABLE}:hover::before {
                color: #333;
            }
            th.${HTML_CLASSES.TABLE_COLUMN_SORTED_ASC}::before {
                content: "\\21D3\\0020";
            }
            th.${HTML_CLASSES.TABLE_COLUMN_SORTED_DESC}::before {
                content: "\\21D1\\0020";
            }
		`;

        CSS += `
            input[type="number"] {
                -webkit-appearance: textfield;
                -moz-appearance: textfield;
                appearance: textfield;
            }
            input[type=number]::-webkit-inner-spin-button,
            input[type=number]::-webkit-outer-spin-button {
                -webkit-appearance: none;
            }
		`;

        const MONTH_NAMES = ["JANUAR", "FEBRUAR", "MÄRZ", "APRIL", "MAI", "JUNI", "JULI", "AUGUST", "SEPTEMBER" ,"OKTOBER" ,"NOVEMBER" ,"DEZEMBER"];

        const REGEX = {
            DOKU: /\.DO[KC]U/i,
            SERIE: /\.S\d\d?(E\d\d?)?(-S\d\d?(E\d\d?)?)?\./i
        };

        return {
            URL_PATH: URL_PATH,
            TABLE_BEITRAEGE_HEADER_NAMES: TABLE_BEITRAEGE_HEADER_NAMES,
            HTML_CLASSES: HTML_CLASSES,
            HTML_IDS: HTML_IDS,
            CSS: CSS,
            MONTH_NAMES: MONTH_NAMES,
            DATA_SIZE_UNITS: DATA_SIZE_UNITS,
            REGEX: REGEX
         };
    })();

    const Util = (() => {
        return {
            getPageAsync: (url, onSuccess, onError = () => {}) => {
                return GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: (resp) => {
                        switch (resp.status) {
                            case 200:
                            case 304:
                                resp.html = new DOMParser().parseFromString(resp.responseText, 'text/html');
                                onSuccess(resp);
                                return;
                            default:
                                onError(resp);
                                return;
                        }
                    },
                    onerror: onError
                });
            },

            createInputElement: (id, type, width, maxLength = 3, textAlign = "left") => {
                const inputElement = document.createElement("INPUT");
                inputElement.id = id;
                inputElement.type = type;
                inputElement.maxLength = maxLength;
                inputElement.style = `text-align:${textAlign};width:${width}`;
                return inputElement;
            },

            createShowContainer: () => {
                const oldContainer = document.getElementById(CONST.HTML_IDS.SHOW_CONTAINER);
                if (!oldContainer) {
                    const divHead = document.getElementById(CONST.HTML_IDS.HEAD);
                    divHead.parentNode.removeChild(divHead);

                    const divContent = document.getElementById(CONST.HTML_IDS.CONTENT);
                    divContent.innerHTML = "";

                    const newContainer = divContent.insertBefore(document.createElement("DIV"), divContent.firstChild);
                    newContainer.id = CONST.HTML_IDS.SHOW_CONTAINER;

                    const divBeitragInfo = newContainer.appendChild(document.createElement("DIV"));
                    divBeitragInfo.id = CONST.HTML_IDS.BEITRAG_INFO;

                    divBeitragInfo.appendChild(document.createTextNode("Seite: "));
                    Object.assign(divBeitragInfo.appendChild(document.createElement("BUTTON")), { innerHTML: "&#8249;", className: CONST.HTML_CLASSES.PREVIOUS, id: CONST.HTML_IDS.BUTTON_PAGE_PREV });
                    divBeitragInfo.appendChild(Util.createInputElement(CONST.HTML_IDS.PAGE_NUMBER, "number", "2em", 3, "center"));
                    Object.assign(divBeitragInfo.appendChild(document.createElement("BUTTON")), { innerHTML: "&#8250;", className: CONST.HTML_CLASSES.NEXT, id: CONST.HTML_IDS.BUTTON_PAGE_NEXT });

                    divBeitragInfo.appendChild(document.createTextNode(", Beitrag: "));
                    Object.assign(divBeitragInfo.appendChild(document.createElement("BUTTON")), { innerHTML: "&#8249;",className: CONST.HTML_CLASSES.PREVIOUS, id: CONST.HTML_IDS.BUTTON_BEITRAG_PREV });
                    divBeitragInfo.appendChild(Util.createInputElement(CONST.HTML_IDS.BEITRAG_NUMBER, "number", "2em", 2, "center"));
                    Object.assign(divBeitragInfo.appendChild(document.createElement("BUTTON")), { innerHTML: "&#8250;", className: CONST.HTML_CLASSES.NEXT, id: CONST.HTML_IDS.BUTTON_BEITRAG_NEXT });

                    divBeitragInfo.appendChild(document.createTextNode(" / "));
                    divBeitragInfo.appendChild(Util.createInputElement(CONST.HTML_IDS.BEITRAG_COUNT, "number", "2em", 2, "center")).disabled = true;

                    divBeitragInfo.appendChild(document.createTextNode(", IMDb: "));
                    divBeitragInfo.appendChild(Util.createInputElement(CONST.HTML_IDS.FILM_DATA_IMDB_RATING, "text", "3em", 4, "center")).disabled = true;

                    divBeitragInfo.appendChild(document.createTextNode(", Größe: "));
                    divBeitragInfo.appendChild(Util.createInputElement(CONST.HTML_IDS.FILM_DATA_GROESSE, "text", "7em", 9, "center")).disabled = true;

                    return true;
                } else {
                    for (let divBeitragOld of oldContainer.querySelectorAll(`DIV.${CONST.HTML_CLASSES.BEITRAG}`)) {
                        oldContainer.removeChild(divBeitragOld);
                    }
                    return false;
                }
            },

            parseInt: (str, maxIntPlaces = 1, fallbackValue = null) => {
                let regex = "\\d";
                if (maxIntPlaces > 1) {
                    regex += "{1," + maxIntPlaces + "}";
                }
                return Util.parseIntWithRegex(str, new RegExp(regex), fallbackValue);
            },

            parseFloat: (str, maxIntPlaces = 1, maxDecPlaces = 0, fallbackValue = null) => {
                let regex = "\\d";
                if (maxIntPlaces > 1) {
                    regex += "{1," + maxIntPlaces + "}";
                }
                if (maxDecPlaces > 0) {
                    regex += "\\s?[,\.]?\\s?\\d{0," + maxDecPlaces + "}";
                }
                return Util.parseFloatWithRegex(str, new RegExp(regex), fallbackValue);
            },

            parseIntWithRegex: (str, regex, fallbackValue = null) => {
                const match = Util.parseStrWithRegex(str, regex);
                const value = parseInt(match);
                return Number.isNaN(value) ? fallbackValue : value;
            },

            parseFloatWithRegex: (str, regex, fallbackValue = null) => {
                const match = Util.parseStrWithRegex(str, regex, "").replace(",",".");
                const value = parseFloat(match);
                return Number.isNaN(value) ? fallbackValue : value;
            },

            parseStrWithRegex: (str, regex, fallbackValue = null) => {
                const match = regex.exec(str || "");
                return (match && match.length >= 1) ? match[0] : fallbackValue;
            },

            monthNameToNumber: (monthName) => {
                return CONST.MONTH_NAMES.indexOf(String(monthName).toUpperCase());
            },

            createTable(data, cellAttributes = {}, headerAttributes = cellAttributes) {
                function configRow(cellElement, content, attributes) {
                    cellElement.appendChild(content instanceof Element ? content : document.createTextNode(String(content)));
                    for (let attrName of Object.keys(attributes)) {
                        cellElement[attrName] = attributes[attrName];
                    }
                }

                const table = document.createElement("TABLE");
                if (data) {
                    for (let element of data) {
                        const row = table.insertRow();

                        for (let key of Object.keys(element)) {
                            const td = row.insertCell();
                            const content = element[key];
                            const attributes = cellAttributes[key] || {};
                            configRow(td, content, attributes);
                        }
                    }

                    const thead = table.createTHead();
                    const row = thead.insertRow();

                    for (let key of Object.keys(data[0])) {
                        const th = document.createElement("TH");
                        const attributes = headerAttributes[key] || {};
                        row.appendChild(th);

                        configRow(th, key, attributes);
                    }
                }

                return table;
            },
        };
    })();

    const Beitrag = (() => {
        const computeImdbLink = (containerElement, fallbackValue = null) => {
            for (let a of Array.from(containerElement.getElementsByTagName("A"))) {
                const linkTextUpper = a.textContent.toUpperCase();
                if (linkTextUpper.includes("IMDB")) {
                    return a;
                }
            }

            for (let elStrong of containerElement.querySelectorAll("STRONG, B")) {
                if (elStrong.textContent.toUpperCase().includes("IMDB")) {
                    const nextEl = elStrong.nextSibling;
                    if (nextEl && nextEl.tagName === "A") {
                        return nextEl;
                    }
                }
            }

            return fallbackValue;
        };

        const computeImdbRating = (containerElement, fallbackValue = null) => {
            const a = computeImdbLink(containerElement);
            if (a !== null) {
                let rating = Util.parseFloat(a.textContent.toUpperCase(), 2, 1);
                if (rating === null) {
                    const nextEl = a.nextSibling;
                    if (nextEl && nextEl.nodeType === Node.TEXT_NODE) {
                        rating = Util.parseFloat(nextEl.textContent.toUpperCase(), 2, 1);
                    }
                }

                if (rating !== null) {
                    return rating;
                }
            }

            return fallbackValue;
        };

        const computeDauer = (containerElement, fallbackValue = null) => {
            for (let elStrong of containerElement.querySelectorAll("STRONG, B")) {
                if (elStrong.textContent.toUpperCase().includes("DAUER")) {
                    const nextEl = elStrong.nextSibling;
                    if (nextEl) {
                        const textContentUpper = nextEl.textContent.toUpperCase();
                        const hours = Util.parseFloatWithRegex(textContentUpper, /(\d{1,2}\s?[,\.]?\s?\d{0,2})\s?(H|STUNDEN|HOURS)/);
                        const minutes = Util.parseIntWithRegex(textContentUpper, /(\d{1,4})\s?(M|MIN|MINUTEN|MINUTES)/);
                        if (hours !== null || minutes !== null) {
                            return (hours === null ? 0 : hours * 60) + (minutes === null ? 0 : minutes);
                        }
                    }
                }
            }
            return fallbackValue;
        };

        const computeMirrors = (containerElement) => {
            return Array.from(containerElement.querySelectorAll("STRONG + A, B + A")).filter(a => {
                const strong = a.previousElementSibling;
                const strongTextContentUpper = strong.textContent.toUpperCase();
                return ["DOWNLOAD", "MIRROR"].some(str => strongTextContentUpper.includes(str));
            }).filter(a => {
                const isWerbeLink = !a.hostname || a.hostname.toUpperCase().includes("MOVIE-BLOG");
                return !isWerbeLink;
            });
        };

        const computeDatum = (containerElement, fallbackValue = null) => {
            const reDay = "\\d{1,2}";
            const reMonth = CONST.MONTH_NAMES.reduce((a, b) => a + "|" + b);
            const reYear = "\\d{4}";
            const reHour = "\\d{1,2}";
            const reMin = "\\d{2}";

            for (let elDatum of containerElement.getElementsByClassName(CONST.HTML_CLASSES.DATE)) {
                const match = new RegExp("(" + reDay + ")\\.\\s(" + reMonth + ")\\s(" + reYear + ")\\s(" + reHour + "):(" + reMin + ")", "i").exec(elDatum.textContent);
                if (match && match.length === 6) {
                    const day = Number.parseInt(match[1]);
                    const month = Util.monthNameToNumber(match[2]);
                    const year = Number.parseInt(match[3]);
                    const hour = Number.parseInt(match[4]);
                    const minute = Number.parseInt(match[5]);

                    return (new Date(year, month, day, hour, minute));
                }
            }
            return fallbackValue;
        };

        const computeLink = (containerElement, fallbackValue = null) => {
            const datum = computeDatum(containerElement);
            if (datum !== null) {
                for (let h1 of containerElement.getElementsByTagName("H1")) {
                    for (let a of h1.getElementsByTagName("A")) {
                        if (a.title && a.title.includes("Permanent Link to")) {
                            return a.cloneNode(true);
                        }
                    }
                }
            }
            return fallbackValue;
        };

        const computeGroesse = (containerElement, fallbackValue = null) => {
            for (let elStrong of containerElement.querySelectorAll("STRONG, B")) {
                if (/GR(OE|Ö)(SS|ß)E/i.test(elStrong.textContent)) {

                    const nextEl = elStrong.nextSibling;
                    if (nextEl) {
                        const textContentUpper = nextEl.textContent.toUpperCase();
                        const gb = Util.parseFloatWithRegex(textContentUpper, /(\d{1,4}\s?[,\.]?\s?\d{0,3})\s?(GB|GIGABYTE)/);
                        const mb = Util.parseIntWithRegex(textContentUpper, /(\d{1,7}?)\s?(MB|MEGABYTE)/);
                        if (gb !== null || mb !== null) {
                            return (gb === null ? 0 : gb * 1024) + (mb === null ? 0 : mb);
                        }
                    }
                }
            }

            return fallbackValue;
        };

        const computeImgElement = (containerElement, fallbackValue = null) => {
            const img = containerElement.querySelectorAll("P > IMG");
            return img.length > 0 ? img[0] : fallbackValue;
        };

        const defaultCompare = new Comparator(...[
            beitrag => [Beitrag.TYPEN.FILM, Beitrag.TYPEN.DOKU, Beitrag.TYPEN.SERIE, Beitrag.TYPEN.WERBUNG].indexOf(beitrag.typ),
            beitrag => beitrag.groesse > 25 * 1024,
            new Comparator(beitrag => beitrag.imdbRating).reverse()
        ]).compareFnc;

        const TYPEN = Object.freeze({
            FILM: "Film",
            SERIE: "Serie",
            DOKU: "Doku",
            WERBUNG: "Werbung"
        });

        // private fields
        const _divElement = new PrivateProperty();
        const _imdbLink = new PrivateProperty();
        const _imdbRating = new PrivateProperty();
        const _imdbId = new PrivateProperty();
        const _dauer = new PrivateProperty();
        const _groesse = new PrivateProperty();
        const _link = new PrivateProperty();
        const _mirrors = new PrivateProperty();
        const _imgElement = new PrivateProperty();
        const _typ = new PrivateProperty();
        const _datum = new PrivateProperty();
        const _releasedYear = new PrivateProperty();

        class Beitrag {
            constructor(divElement) {
                _divElement.set(this, divElement);
            }

            get divElement() {
                return _divElement.get(this);
            }

            get imdbLink() {
                return _imdbLink.getOrCompute(this, () => computeImdbLink(this.divElement, null));
            }

            get imdbRating() {
                return _imdbRating.getOrCompute(this, () => computeImdbRating(this.divElement, -1));
            }

            get imdbRatingAsText() {
                return this.imdbRating === -1 ? "" : this.imdbRating.toFixed(1);
            }

            get imdbId() {
                return _imdbId.getOrCompute(this, () => this.imdbLink === null ? "" : Util.parseStrWithRegex(this.imdbLink, /tt\d{7,8}/, ""));
            }

            get dauer() {
                return _dauer.getOrCompute(this, () => computeDauer(this.divElement, 0));
            }

            get dauerAsText() {
                if (this.dauer === 0) {
                    return "";
                }
                const hours = String(Math.floor(this.dauer / 60));
                const minutes = String(this.dauer % 60);
                return `${hours}:${minutes.length === 1 ? "0" + minutes : minutes}`;

            }

            get groesse() {
                return _groesse.getOrCompute(this, () => computeGroesse(this.divElement, 0));
            }

            /**
             * Returns value "groesse" of this object converted to a specified unit.
             *
             * @param {string} [unit]          - Unit ("B", "KB", "MB", "GB", "TB"), see CONST.DATA_SIZE_UNITS
             * @returns {number}               - value converted for specified unit
             */
            getGroesseInUnit(unit) {
                const defaultDataSize = CONST.DATA_SIZE_UNITS.get("MB");
                const targetDataSize = CONST.DATA_SIZE_UNITS.get(unit) || defaultDataSize;
                return this.groesse * (defaultDataSize.factor / targetDataSize.factor);
            }

            /**
             * Returns a formatted string of attribute "groesse" of this object
             *
             * @param {string} [unit]          - Unit ("B", "KB", "MB", "GB", "TB"), see CONST.DATA_SIZE_UNITS
             * @param {number} [decimalPlaces] - Number of fixed digits after the decimal point
             * @returns {string}               - formatted string: "value unit", e.g. "2.61 GB"
             */
            getGroesseAsText(unit, decimalPlaces) {
                if (this.groesse === 0) {
                    return "";
                }

                let value = this.groesse;

                const defaultDataSize = CONST.DATA_SIZE_UNITS.get("MB");
                let dataSize = CONST.DATA_SIZE_UNITS.get(unit);

                if (!dataSize) {
                    for (dataSize of Array.from(CONST.DATA_SIZE_UNITS.values()).filter(ds => ds.factor >= defaultDataSize.factor)) {
                        if (dataSize.factor > defaultDataSize.factor) {
                            value = value / 1024;
                            if (typeof decimalPlaces === "undefined") {
                                decimalPlaces = 1;
                            }
                        }

                        if (value < 1024) {
                            break;
                        }
                    }
                } else {
                    value *= (defaultDataSize.factor / dataSize.factor);
                }

                return Number(Math.round(value + 'e1') + 'e-1').toFixed(decimalPlaces || 0) + " " + dataSize.shortText;
            }

            get link() {
                return _link.getOrCompute(this, () => computeLink(this.divElement, null));
            }

            get linkText() {
                return this.link ? this.link.textContent : "";
            }

            get mirrors() {
                return _mirrors.getOrCompute(this, () => computeMirrors(this.divElement));
            }

            get imgElement() {
                return _imgElement.getOrCompute(this, () => computeImgElement(this.divElement));
            }

            get typ() {
                return _typ.getOrCompute(this, () => {
                    if (this.mirrors.length === 0 || this.linkText.toUpperCase().includes('-HELD')) {
                        return Beitrag.TYPEN.WERBUNG;
                    }

                    if (CONST.REGEX.DOKU.test(this.linkText)) {
                        return Beitrag.TYPEN.DOKU;
                    }

                    if (CONST.REGEX.SERIE.test(this.linkText)) {
                        return Beitrag.TYPEN.SERIE;
                    }
                    return "";
                });
            }

            get datum() {
                return _datum.getOrCompute(this, () => computeDatum(this.divElement, null));
            }

            get releasedYear() {
                return _releasedYear.getOrCompute(this, () => {
                    const match = /\.(\d{4})\./.exec(this.linkText);
                    if (!match || match.length <= 1) {
                        return null;
                    }
                    return parseInt(match[1]) || null;
                });
            }

            static get TYPEN() {
                return TYPEN;
            }

            static get defaultCompare() {
                return defaultCompare;
            }
        }

        return Beitrag;
    })();


    const View = (() => {
        const addBeitraegeAsync = (beitraege, fromNumOfPage, toNumOfPage, onSuccess, onError) => {
            const urlPath = document.location.href.replace('page/', '').replace('#','');
            Util.getPageAsync(fromNumOfPage == 0 ? urlPath : `${urlPath}page/${fromNumOfPage + 1}/`, resp => {

                // add Beitraege
                beitraege.push(...Array.from(resp.html.querySelectorAll(`DIV.${CONST.HTML_CLASSES.BEITRAG}`)).map(div => new Beitrag(div)));

                if (fromNumOfPage < toNumOfPage) {
                    // load further Beitraege
                    addBeitraegeAsync(beitraege, fromNumOfPage + 1, toNumOfPage, onSuccess, onError);
                    return;
                }

                // Beitreage of last page loaded successfully
                onSuccess();
            }, onError);
        };

        return class {
            constructor(numOfPage = 0, numOfBeitrag = 0, pageSteps = CONFIG.PAGES_LOAD_COUNT) {
                this.pageSteps = pageSteps;
                this.numOfPage = numOfPage;
                this.numOfBeitrag = numOfBeitrag;
                this.beitraege = [];

                this.gotoPage();
            }

            get beitragCount() {
                return this.beitraege.length;
            }

            setBrowsingEnabled(isBrowsingEnabled) {
                this.isPageLoading = !isBrowsingEnabled;
                [
                    document.getElementById(CONST.HTML_IDS.PAGE_NUMBER),
                    document.getElementById(CONST.HTML_IDS.BEITRAG_NUMBER),
                    document.getElementById(CONST.HTML_IDS.BUTTON_PAGE_PREV),
                    document.getElementById(CONST.HTML_IDS.BUTTON_PAGE_NEXT),
                    document.getElementById(CONST.HTML_IDS.BUTTON_BEITRAG_PREV),
                    document.getElementById(CONST.HTML_IDS.BUTTON_BEITRAG_NEXT),
                ].forEach(inputElement => {
                    if (inputElement) {
                        inputElement.disabled = !isBrowsingEnabled;
                    }
                });
            }

            gotoPage(numOfPage = this.numOfPage, numOfBeitrag = -1) {
                // disable page browsing
                this.setBrowsingEnabled(false);

                const onSuccess = () => {
                    if (0 <= numOfBeitrag && numOfBeitrag < this.beitragCount) {
                        this.numOfBeitrag = numOfBeitrag;
                    } else {
                        this.numOfBeitrag = (numOfPage < this.numOfPage) ? this.beitragCount - 1 : 0;
                    }
                    this.numOfPage = numOfPage;

                    // distinct for links
                    const beitraegeByLinks = new Map(this.beitraege.map(b => [ b.link.href, b ]));
                    this.beitraege = Array.from(beitraegeByLinks.values());

                    // sort Beitraege by IMDb rating
                    this.beitraege.sort((a, b) => Beitrag.defaultCompare(a, b));

                    this.showBeitrag();
                    this.showTable();
                    this.highlightTableRow();
                    this.setBrowsingEnabled(true);
                };
                const onError = resp => this.setBrowsingEnabled(true);

                // merge Beitraege of 'pageSteps' pages
                this.beitraege = [];

                let numOfPageFirst = numOfPage * this.pageSteps;
                let numOfPageLast = numOfPageFirst + this.pageSteps - 1;
                addBeitraegeAsync(this.beitraege, numOfPageFirst, numOfPageLast, onSuccess, onError);
            }

            indexOfBeitrag(beitrag) {
                return this.beitraege.indexOf(beitrag);
            }

            gotoBeitrag(numOfBeitrag = this.numOfBeitrag) {
                this.highlightTableRow(this.numOfBeitrag, false);

                this.numOfBeitrag = numOfBeitrag;
                this.showBeitrag();

                this.highlightTableRow(this.numOfBeitrag, true);
            }

            showBeitrag() {
                // get a clean show container
                const isNewShowContainer = Util.createShowContainer();

                // clone Beitrag into show container
                const beitrag = this.beitraege[this.numOfBeitrag];
                const divBeitrag = beitrag ? beitrag.divElement : null;
                if (divBeitrag) {
                    const beitragContent = document.getElementById(CONST.HTML_IDS.SHOW_CONTAINER);
                    const beitragInfo = document.getElementById(CONST.HTML_IDS.BEITRAG_INFO);

                    const inputPageNum = document.getElementById(CONST.HTML_IDS.PAGE_NUMBER);
                    inputPageNum.value = this.numOfPage + 1;
                    inputPageNum.min = 1;
                    inputPageNum.max = 9999;

                    const inputBeitragNum = document.getElementById(CONST.HTML_IDS.BEITRAG_NUMBER);
                    inputBeitragNum.value = this.numOfBeitrag + 1;
                    inputBeitragNum.min = 1;
                    inputBeitragNum.max = this.beitragCount;

                    const btnPagePrev = document.getElementById(CONST.HTML_IDS.BUTTON_PAGE_PREV);
                    const btnPageNext = document.getElementById(CONST.HTML_IDS.BUTTON_PAGE_NEXT);
                    const btnBeitragPrev = document.getElementById(CONST.HTML_IDS.BUTTON_BEITRAG_PREV);
                    const btnBeitragNext = document.getElementById(CONST.HTML_IDS.BUTTON_BEITRAG_NEXT);

                    const gotoPage = (pageNum) => {
                        if (!this.isPageLoading) {
                            this.gotoPage(Math.max(0, pageNum), 0);
                        }
                    };

                    const gotoBeitrag = (beitragNum) => {
                        if (!this.isPageLoading) {
                            beitragNum = Math.max(beitragNum, 0);
                            beitragNum = Math.min(beitragNum, this.beitragCount - 1);
                            this.gotoBeitrag(beitragNum);
                        }
                    };

                    if (isNewShowContainer) {
                        inputPageNum.addEventListener("change", () => {
                            gotoPage(parseInt(inputPageNum.value) - 1);
                        });
                        btnPagePrev.addEventListener("click", () => {
                            gotoPage(parseInt(inputPageNum.value) - 1 - 1);
                        });
                        btnPageNext.addEventListener("click", () => {
                            gotoPage(parseInt(inputPageNum.value) - 1 + 1);
                        });

                        inputBeitragNum.addEventListener("change", () => {
                            gotoBeitrag(parseInt(inputBeitragNum.value) - 1);
                        });
                        btnBeitragPrev.addEventListener("click", () => {
                            gotoBeitrag(parseInt(inputBeitragNum.value) - 1 - 1);
                        });
                        btnBeitragNext.addEventListener("click", () => {
                            gotoBeitrag(parseInt(inputBeitragNum.value) - 1 + 1);
                        });
                    }

                    document.getElementById(CONST.HTML_IDS.BEITRAG_COUNT).value = this.beitragCount;
                    document.getElementById(CONST.HTML_IDS.FILM_DATA_IMDB_RATING).value = beitrag.imdbRating;
                    document.getElementById(CONST.HTML_IDS.FILM_DATA_GROESSE).value = beitrag.getGroesseAsText();

                    // remove former image from sidebar
                    const curImage = document.getElementById(CONST.HTML_IDS.CUR_BEITRAG_IMG);
                    if (curImage !== null) {
                        curImage.parentElement.removeChild(curImage);
                    }

                    // insert image to sidebar
                    const divSidebar = document.getElementById(CONST.HTML_IDS.SIDEBAR);
                    const imgElement = beitrag.imgElement;
                    if (divSidebar && imgElement) {
                        imgElement.id = CONST.HTML_IDS.CUR_BEITRAG_IMG;
                        divSidebar.insertBefore(imgElement, divSidebar.firstChild);

                        window.onscroll = () => {
                            imgElement.style.display = ((window.pageYOffset || document.documentElement.scrollTop) < CONFIG.SIDEBAR_POSITION_TOP_PX - 175) ? 'block' : 'none';
                        };
                    }

                    return beitragContent.appendChild(divBeitrag) ? true : false;
                }
                return false;
            }

            highlightTableRow(numOfBeitrag = this.numOfBeitrag, isHighlight = true) {
                const tableRow = document.getElementById(`${CONST.HTML_IDS.TABLE_TR_NUM}_${numOfBeitrag + 1}`);
                if (tableRow) {
                    tableRow.classList[isHighlight ? "add" : "remove"](CONST.HTML_CLASSES.TABLE_ROW_HIGHLIGHTED);
                }
            }

            showTable() {
                const sortByColumn = ({containerElement, columnClass, sortFunction, defaultSortOrderAsc = true} = {}) => {
                    const curBeitrag = this.beitraege[this.numOfBeitrag];

                    if (this.sortedColumnClass !== columnClass) {
                        const thSortedPrevious = this.sortedColumnClass ? null : containerElement.querySelector(`th.${this.sortedColumnClass}`);
                        if (thSortedPrevious) {
                            thSortedPrevious.classList.remove(CONST.HTML_CLASSES.TABLE_COLUMN_SORTED_ASC, CONST.HTML_CLASSES.TABLE_COLUMN_SORTED_DESC);
                        }

                        this.beitraege.sort(sortFunction);
                        this.sortedColumnClass = columnClass;
                        this.sortOrder = defaultSortOrderAsc;
                    } else {
                        this.beitraege.reverse();
                        this.sortOrder = !this.sortOrder;
                    }

                    this.showTable();
                    this.gotoBeitrag(this.indexOfBeitrag(curBeitrag));

                    const thSort = containerElement.querySelector(`th.${this.sortedColumnClass}`);
                    if (thSort) {
                        thSort.classList.add(this.sortOrder ? CONST.HTML_CLASSES.TABLE_COLUMN_SORTED_ASC : CONST.HTML_CLASSES.TABLE_COLUMN_SORTED_DESC);
                    }
                };

                let tableDiv = document.getElementById(CONST.HTML_IDS.LISTE_BEITRAEGE);
                if (!tableDiv) {
                    const beitragInfoDiv = document.getElementById(CONST.HTML_IDS.BEITRAG_INFO);
                    const beitragContent = document.getElementById(CONST.HTML_IDS.SHOW_CONTAINER);
                    tableDiv = beitragInfoDiv.parentNode.insertBefore(document.createElement("DIV"), beitragInfoDiv);
                    tableDiv.id = CONST.HTML_IDS.LISTE_BEITRAEGE;
                } else {
                    while (tableDiv.firstChild) {
                        tableDiv.removeChild(tableDiv.firstChild);
                    }
                }

                const tableData = this.beitraege.map((beitrag, i) => ({
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.NUM]: i + 1,
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.RATING]: (() => {
                        const textContent = beitrag.imdbRatingAsText;
                        if (beitrag.imdbId) {
                           const link = document.createElement("A");
                            link.target = "_blank";
                            link.href = `https://www.imdb.com/title/${beitrag.imdbId}/`;
                            link.textContent = textContent;
                            return link;
                        }
                        return textContent;
                    })(),
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.JAHR]: beitrag.releasedYear || "",
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.LINK]: (() => {
                        if (!beitrag.link) {
                            return "";
                        }

                        const link = beitrag.link.cloneNode(true);
                        // Textformatierungen entfernen
                        while(link.firstChild) {
                            link.removeChild(link.firstChild);
                        }
                        link.textContent = beitrag.linkText;
                        return link;
                    })(),
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DURATION]: beitrag.dauerAsText,
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.SIZE]: beitrag.getGroesseAsText(CONFIG.TABLE_BEITRAEGE_SIZE_DEFAULT_UNIT, 1),
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.TYPE]: beitrag.typ,
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DATE]: (() => {
                        const date = beitrag.datum.toLocaleDateString(undefined, { dateStyle: "short" });
                        const time = beitrag.datum.toLocaleTimeString(undefined, { timeStyle: "short" });
                        return [ date, time ].join(" ");
                    })(),
                }));

                const thisView = this;
                const tdAttributes = {
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.NUM]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_NUM, onmouseover: function() {
                        const lineNumber = parseInt(this.innerText);
                        if (Number.isInteger(lineNumber)) {
                            thisView.gotoBeitrag(lineNumber - 1);
                        }
                    }},
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.RATING]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_RATING },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.JAHR]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_JAHR },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.LINK]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DURATION]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DURATION },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.SIZE]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_SIZE },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.TYPE]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_TYPE },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DATE]: { className: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DATE },
                };

               const thAttributes = {
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.NUM]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_NUM,
                            sortFunction: Beitrag.defaultCompare,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_NUM, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.RATING]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_RATING,
                            sortFunction: new Comparator(beitrag => beitrag.imdbRating).reverse().compareFnc,
                            defaultSortOrderAsc: false
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_RATING, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.JAHR]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_JAHR,
                            sortFunction: new Comparator(beitrag => beitrag.releasedYear).compareFnc,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_JAHR, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.LINK]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK,
                            sortFunction: new Comparator(beitrag => beitrag.linkText).compareFnc,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_LINK, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DURATION]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DURATION,
                            sortFunction: new Comparator(beitrag => beitrag.dauer).compareFnc,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DURATION, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.SIZE]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_SIZE,
                            sortFunction: new Comparator(beitrag => beitrag.groesse).reverse().compareFnc,
                            defaultSortOrderAsc: false
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_SIZE, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.TYPE]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_TYPE,
                            sortFunction: new Comparator(beitrag => beitrag.typ).compareFnc,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_TYPE, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                    [CONST.TABLE_BEITRAEGE_HEADER_NAMES.DATE]: {
                        onclick: () => sortByColumn({
                            containerElement: tableDiv,
                            columnClass: CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DATE,
                            sortFunction: new Comparator(beitrag => beitrag.datum.valueOf()).compareFnc,
                            defaultSortOrderAsc: true
                        }),
                        className: [ CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_DATE, CONST.HTML_CLASSES.TABLE_COLUMN_SORTABLE ].join(" ")
                    },
                };

                const table = Util.createTable(tableData, tdAttributes, thAttributes);
                for (let tr of table.getElementsByTagName("TR")) {
                    const tdNum = tr.getElementsByClassName(CONST.HTML_CLASSES.TABLE_BEITRAEGE_COLUMN_NUM);
                    const num = tdNum.length > 0 ? Util.parseInt(tdNum[0].textContent, 3) : null;
                    if (num !== null) {
                        tr.id = `${CONST.HTML_IDS.TABLE_TR_NUM}_${num}`;
                    }
                }

                tableDiv.appendChild(table);
            }
        };
    })();

    const view = new View();
	GM_addStyle(CONST.CSS);

    const browseBeitrag = function(distance) {
        if (view.isPageLoading) {
            return;
        }
        const numOfNextBeitrag = view.numOfBeitrag + distance;

        if (numOfNextBeitrag < 0) {
            if (view.numOfPage > 0) {
                view.gotoPage(view.numOfPage - 1);
            }
        } else if (numOfNextBeitrag >= view.beitragCount) {
            view.gotoPage(view.numOfPage + 1);
        } else {
            view.gotoBeitrag(numOfNextBeitrag);
        }
    };

    document.onkeydown = function (e) {
        switch (e.keyCode) {
            case 37: // key left
                browseBeitrag(-1);
                return false;
            case 39: // key right
                browseBeitrag(1);
                return false;
            case 33: // page up
                view.gotoPage(Math.max(0, view.numOfPage - 1), 0);
                return false;
            case 34: // page down
                view.gotoPage(view.numOfPage + 1, 0);
                return false;
        }
    };
})();