BetterFxP for Tampermonkey

מעולם לא היה קל יותר לגלוש ב-FxP.

// ==UserScript==
// @name         BetterFxP for Tampermonkey
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  מעולם לא היה קל יותר לגלוש ב-FxP.
// @author       You
// @match        https://www.fxp.co.il/*
// @supportURL   https://discord.gg/AW7CeG7
// @require      https://update.gf.qytechs.cn/scripts/439099/1203718/MonkeyConfig%20Modern%20Reloaded.js
// @icon         https://lh3.googleusercontent.com/j-CdJwaXX0eoqlMDLLYfbYTuuaFUM5Ep-Mph1UNktCZSYbm665WoIwGGw4d1iXxQWkLMDYior_xS8OKfWCBf1i4srw=s120
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_addElement
// @grant        GM_getResourceText
// @grant        GM_registerMenuCommand
// @grant        GM_addValueChangeListener
// @run-at       document-start
// @resource pms https://update.gf.qytechs.cn/scripts/476628/1259426/fxp%20anti-delete%20PMs.user.js
// @license      MIT
// ==/UserScript==
// @noframes
const checkbox = (label) => ({ label, default: false, type: 'checkbox' });
const text = (label, defaultValue = '', opt = {}) => ({ label, default: defaultValue, type: 'text', ...opt });
const cfg = new MonkeyConfig({
    title: 'הגדרות FxPlus+',
    menuCommand: true,
    params: {
        hideBigImages: checkbox("הסתר את הכתבות הגדולות מדך הבית"),
        hideGames: checkbox("הסתר את אזור המשחקים מדף הבית"),
        hideArticles: checkbox("הסתר כתבות מדף הבית"),
        resizeSignatures: checkbox("חותך חתימות גדולות"),
        hideAds: checkbox("הסתר מודעות"),
        hideNagish: checkbox("הסתר את תפריט הנגישות"),
        showFriends: checkbox("הצג חברים באשכולת"),
        showAutoPinned: checkbox("הצג יותר משלושה אשכולות נעוצים"),
        disableLiveTyping: checkbox("אל תודיע שאני מקליד"),
        showDeletedPost: checkbox("הצג פוסט שנמחק"),
        showLikeLimit: checkbox("הצג מגבלת לייקים"),
        connectedStaff: checkbox("הצג צוות מחובר"),
        showCounts: checkbox('מציג את מספר הפוסטים ואת כמות המשתמשים המחוברים'),
        pms: checkbox("מציג הודעות פרטיות שנמחקו"),
        showForumStats: checkbox('הצג סטטיסטיקות פורומים'),
        weeklyChallenge: checkbox('מציג אתגרים שבועיים בתוך הפורום'),
        audioChange: text(":קישור לקובץ שמע עבור התראה"), // https://www.tzevaadom.co.il/static/sounds/calm.wav
        hideCategories: text(":רשימה של קטגוריות להסתרה"), // 4428, 13
        smiles: text(':רשימה של קישורים לסמיילים', '', { long: 3 }), // https://yoursmiles.org/tsmile/heart/t4524.gif
        nightMode: checkbox('הפעל את מצב הלילה אוטומטית'),
        startTime: text('Start Time:', '17:00'),
        endTime: text("End Time:", '23:50'),
        color: text("צבע"),
        font: text('פונט'),
        size: text('גודל'),
        // This is a temporary solution until I find a better library.
        // TODO: md5 the pass
        user1name: text("שם"),
        user1pass: text("סיסמה"),
        user2name: text("שם"),
        user2pass: text("סיסמה"),
    }
});

document.addEventListener('keydown', function (e) {
    if (e.ctrlKey && e.key.toLowerCase() === 'y') {
        e.preventDefault();
        cfg.open("window", {
            windowFeatures: { width: 500 }
        });
    }
});

/*
CKEDITOR.tools.callFunction(41, this); //131,'almoni-dl'
The system that automatically disables and enables the feature is currently not working in either version.
TODO:
- Implement audio file upload
- Support multiple users
- disable/enable on the same page and prevent reload
- BBCode support
- Hide sticky posts
*/
const rawWindow = unsafeWindow;
const queryParams = new URLSearchParams(location.search);

function waitForObject(path) {
    return new Promise((resolve, reject) => {
        const timer = setInterval(() => {
            const obj = path.split('.').reduce((o, key) => (o && key in o ? o[key] : undefined), rawWindow);
            if (typeof obj !== "undefined") {
                clearInterval(timer);
                resolve(obj);
            }
        }, 100);
    });
}

function setCookie(name, value, minutes) {
    const expires = new Date(Date.now() + minutes * 60 * 1000).toUTCString();
    document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
}

function getCookie(n) {
    return decodeURIComponent(document.cookie.split('; ').find(c => c.startsWith(n + '='))?.split('=')[1] || '');
}

function onMatchIfLoggedIn(match, permissions, callback) {
    rawWindow.LOGGEDIN && onMatch(match, permissions, callback);
}

function onMatch(match, permission, callback) {
    const hasPermission = permission === "none" || cfg.get(permission);
    if (!shouldRun(match) || !hasPermission) return;

    const docReady = /complete|interactive/.test(document.readyState);
    const runImmediately = !callback.toString().includes("document");

    let teardown = () => {};
    const executeFeature = () => {
        teardown();
        teardown = callback() || (() => {});
    };

    if (runImmediately || docReady) executeFeature();
    else document.addEventListener('DOMContentLoaded', executeFeature);

    GM_addValueChangeListener(permission, (key, oldVal, newVal) => {
        const func = newVal ? executeFeature : teardown;
        func();
    });
}

function injectStyle(match, permissions, css) {
    onMatch(match, permissions, function() {
        const styleElement = GM_addStyle(css);
        return () => styleElement?.remove();
    });
}

function shouldRun(matchPattern) {
    const urlPath = '/' + location.href.split('/').pop();
    const pattern = new RegExp(matchPattern.replace('*', '.*'));
    return pattern.test(urlPath);
}

async function fetcher(url, opt = {}) {
    const response = await fetch(url, opt);
    return await response.text();
}

function Listener(callback) {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url) {
        this.method = method;
        this.url = url;
        originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function(body) {
        this.body = body;
        this.addEventListener("load", () => {
            callback(this);
        });
        originalSend.apply(this, arguments);
    };
}
// Author ID: 967488
injectStyle("forumdisplay", "showAutoPinned", "#stickies li.threadbit:nth-child(n+4) { display: list-item !important; } .morestick { display: none !important; }");
injectStyle("*", "hideAds", "#adfxp, #related_main, .trc_related_container, .trc_spotlight_widget, .videoyoudiv, .OUTBRAIN { display: none !important }");
injectStyle("*", "hideNagish", ".nagish-button { display: none; }");
injectStyle("/(?:index.php)?", "hideCategories", `${cfg.get("hideCategories").split(", ").map(cId => `.hp_category:has(a[href="forumdisplay.php?f=${cId}"])`).join(',')} { display: none }`) // #cat${cId}, .hi4 { height: 337px }
injectStyle("/(?:index.php)?", "hideArticles", "#slide { height:auto !important; } .mainsik { display: none; }");
injectStyle("/(?:index.php)?", "hideBigImages", "#slide { height:auto !important; } .big-image-class { display: none; }");
injectStyle("/(?:index.php)?", "hideGames", "#slide ~ div h1, .fxp2021_Games { display: none !important;");
// חותך חתימות גדולות לגודל המותר וזה תלוי במשתמש במקום באתר לשנות את החתימה שתתאים
injectStyle("show(post|thread)|member.php", "resizeSignatures", ".signaturecontainer { max-width: 500px; max-height: 295px; overflow: hidden; }");

onMatchIfLoggedIn("private_chat.php?do=showpm|show(post|thread)", "disableLiveTyping", function() {
    const originalSendTypingInThread = rawWindow?.sendUserIsTypingInShowthread;
    const originalTypingSend = rawWindow?.typeingsend;

    rawWindow.sendUserIsTypingInShowthread = () => {};
    rawWindow.typeingsend = () => {};

    return () => {
        rawWindow.sendUserIsTypingInShowthread = originalSendTypingInThread;
        rawWindow.typeingsend = originalTypingSend;
    }
});
onMatchIfLoggedIn("signature", "none", function() {
    const publishedThreadUrl = "https://www.fxp.co.il/showthread.php?t=16859147";

    GM_addStyle(`
        #creditAddon { padding: .5em; text-align: center; }
        .addCreditBtn { margin: .2em 1em; background: #fff; border-radius: .2em; font-weight: 700; color: #004b67; border: 1px solid #c5c5c5; cursor: pointer; padding: 0; position: relative; width: 60px; height: 60px; display: inline-flex; justify-content: center; align-items: center; overflow: hidden; }
        .addCreditBtn img { border: 0; height: 100%; }
        .addCreditBtn .addCreditDesc { position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; color: #fff; background: rgba(0,0,0,.4); opacity: 1; }
        .addCreditBtn .addCreditDesc:hover { opacity: 0; }
        .addCreditBtn#addTextCredit .addCreditDesc:hover { opacity: 1; }
        .addCreditBtn#addXLimg { width: 110px; }
    `);

    const creditAddon = GM_addElement('div', {
        id: 'creditAddon',
    })
    creditAddon.innerHTML = `
    <div>שתף את הכיף!™ והוסף קרדיט לתוסף +FxPlus בחתימה שלך:</div>
    <div class="addCreditBtn" id="addLimg">
        <img src="https://i.imagesup.co/images2/b059514f80af8c5ec69afc73356a4dfa3b771343.png">
        <span class="addCreditDesc">128x128</span>
    </div>
    <div class="addCreditBtn" id="addMimg">
        <img src="https://i.imagesup.co/images2/7797f421f0e4895878e51d09266a35355b214d5a.png">
        <span class="addCreditDesc">48x48</span>
    </div>
    <div class="addCreditBtn" id="addTextCredit">
        <span class="addCreditDesc">טקסט</span>
    </div>`

    creditAddon.querySelectorAll('#addLimg, #addMimg, #addTextCredit').forEach(element => {
        element.addEventListener('click', (event) => {
            const imgElement = event.target.parentElement.querySelector('img');
            const iframeBody = document.querySelector(".cke_contents iframe").contentDocument.body;
            
            const creditLink = GM_addElement(iframeBody, 'a', {
                href: publishedThreadUrl, target: '_blank'
            });

            if (!imgElement) creditLink.textContent = '+FxPlus';
            else GM_addElement(creditLink, "img", { src: imgElement.src });
        });
    });
    const smilieBox = document.querySelector('form[action*="signature"] .editor_smiliebox');
    smilieBox.parentNode.insertBefore(creditAddon, smilieBox);
});

onMatchIfLoggedIn("*", "showFriends", async function () {
    const storageKey = "refreshFriends";
    if (getCookie(storageKey)) return;

    const allFriendIds = [];
    const regex = /<h4><a href="member\.php\?u=(\d+)"/g;
    let page = 1;
    
    while (true) {
        const url = `https://www.fxp.co.il/profile.php?do=buddylist&pp=100&page=${page}`;
        const html = await fetcher(url);

        const matches = Array.from(html.matchAll(regex)).map(m => m[1]);
        matches.forEach(id => allFriendIds.push(id));

        if (matches.length < 100) break;

        page++;
    }

    GM_setValue('friendIds', JSON.stringify(allFriendIds));
    setCookie(storageKey, Date.now(), 15);
});

onMatchIfLoggedIn("show(post|thread)", "showFriends", function() {
    const friendIds = JSON.parse(GM_getValue("friendIds", '[0]'));
    const styleElement = GM_addStyle(`
        ${friendIds.map(id => '.username[href$="' + id + '"]::after').join(', ')} {
            content: "";
            display: inline-block;
            width: 20px;
            height: 20px;
            background-image: url('https://w7.pngwing.com/pngs/236/25/png-transparent-computer-icons-avatar-friends-love-text-logo-thumbnail.png');
            background-size: cover;
    }`)
    return () => styleElement?.remove()
})
onMatchIfLoggedIn("*", "audioChange", function() {
    let isFeatureEnabled = true;
    Object.defineProperty(HTMLAudioElement.prototype, 'src', {
        set: function(value) {
            if (isFeatureEnabled && value === "https://images4.fxp.co.il/nodejs/sound.mp3") {
                value = cfg.get("audioChange");
            }
            this.setAttribute('src', value);
        },
        get: () => this.getAttribute('src'),
        configurable: true,
        enumerable: true,
    });
    return () => {
        isFeatureEnabled = false;
    }
})
onMatchIfLoggedIn("show(post|thread)", "smiles", async function() {
    const images = cfg.get("smiles").trim().split('\n');
    if (images.length < 1) return;

    const editor = await waitForObject("vB_Editor.vB_Editor_QR");
    let originalDescriptions = editor.config.smiley_descriptions,
        originalImages = editor.config.smiley_images;

    for (const image of images) {
        if (!image) continue;
        editor.config.smiley_descriptions.push(`[img]${image}[/img]`);
        editor.config.smiley_images.push(`https://wsrv.nl/?url=${image}&w=30`);
    }

    return () => {
        editor.config.smiley_descriptions = originalDescriptions;
        editor.config.smiley_images = originalImages;
    }
})
onMatchIfLoggedIn("show(post|thread)", "showLikeLimit", async function() {
    let toRemove = [];
    // For now, this is good. In the future, consider verifying the full name and not just if it includes certain text (to detect fake accounts).
    async function checkLike(postid) {
        const response = await fetcher("https://www.fxp.co.il/ajax.php", {
            method: "POST",
            headers: {
                "content-type": "application/x-www-form-urlencoded",
            },
            body: `do=wholikepost&postid=${postid}&securitytoken=${rawWindow.SECURITYTOKEN}`,
        });
        return response.includes(rawWindow.my_user_name);
    }
    Listener(async e => {
        if (e.method !== "POST" || e.url !== "ajax.php") {
            return;
        }
        const postId = e.body.match(/\d+/);
        if (!postId || await checkLike(postId)) return;
        
        const element = document.getElementById(`${postId}_removelike`);
        element.style.backgroundImage = 'url("https://em-content.zobj.net/source/google/387/broken-heart_1f494.png")';
        toRemove.push(element);
    });

    return () => {
        toRemove.forEach(el => el.style.backgroundImage = '');
        toRemove = [];
    }
})
// https://gf.qytechs.cn/en/scripts/476628-fxp-anti-delete-pms
onMatchIfLoggedIn("do=showpm&pmid=", "pms", async function() {
    await waitForObject("socket");
    new Function(GM_getResourceText("pms")).apply(rawWindow);
})
onMatch("show(post|thread)", "showDeletedPost", function() {
    const targetPostId = queryParams.get('p');
    const isPostExist = document.contains(document.getElementById('post_' + targetPostId));
    if (!targetPostId || isPostExist) return;
    const elements = Array.from(document.querySelectorAll('.postbit'));
    const postIds = elements.map(el => parseInt(el.id.replace('post_', '')));
    const index = postIds.filter(pid => pid < targetPostId).length - 1;
    // if (index === 0) index = 1; //need to be test
    const newElement = GM_addElement("li", {
        textContent: 'התגובה שאתה מנסה לראות נמחקה.',
        id: 'post_' + targetPostId,
        class: 'postbit postbitim postcontainer', //test how this show (innerHTML)
        style: "background-color: #ffdddd; border: 1px solid #ff0000; padding: 10px 0; border-radius: 5px; color: #333; font-weight: bold; text-align: center;"
    })
    const targetElement = elements.at(index);
    targetElement.parentNode.insertBefore(newElement, targetElement.nextSibling);

    if (!queryParams.has('t')) {
        setTimeout(() => targetElement.scrollIntoView(), 500);
    }

    return () => newElement?.remove();
});
onMatch("forumdisplay", "connectedStaff", function() {
    const team = document.querySelector(".teammen.flo");
    team.dir = "auto"
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '10px');
    svg.setAttribute('height', '10px');
    svg.setAttribute('viewBox', '0 0 24 24');

    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('fill', '#00FF00');
    path.setAttribute('d', 'm2 12a10 10 0 1 1 10 10 10 10 0 0 1 -10-10z');

    svg.appendChild(path);

    const usernameElements = document.querySelectorAll('.flo .username');

    usernameElements.forEach(async usernameElement => {
        const username = usernameElement.innerText;
        const userLink = usernameElement.href;

        const html = await fetcher(userLink);
        if (html.includes(username + ' מחובר/ת')) {
            usernameElement.insertAdjacentHTML('beforeend', svg.outerHTML);
        }
    });

    return () => {
        team.dir = "";
        usernameElements.forEach(({
            lastChild
        }) => lastChild.tagName === 'svg' && lastChild.remove());
    }
});
onMatch("*", "showCounts", function() {
    const scripts = document.querySelectorAll('script[type="text/javascript"]:not([src])');
    const script = Array.from(scripts).find(e => e.textContent.includes("counts"));
    if (!script) return;

    const lines = script.innerText.split("\n");
    const counts = {};
    for (const line of lines) {
        const match = line.match(/counts\["(.+?)"\]\s*=\s*(\d+);/);
        if (match) counts[match[1]] = parseInt(match[2]);
    }

    const container = GM_addElement("div", {
        style: `
            position: fixed;
            bottom: 10px;
            right: 10px;
            background: white;
            color: white;
            border: 1px solid #ccc;
            padding: 10px 15px;
            box-shadow: 0 0 5px rgba(0,0,0,0.2);
            border-radius: 8px;
            z-index: 9999;
            font-family: Arial, sans-serif;
            font-size: 14px;
            transition: opacity 1s`
    });
    container.innerHTML = `
        <strong>סטטיסטיקה:</strong><br>
        מחוברים: ${counts["#total_online"]?.toLocaleString() ?? 'N/A'}<br>
        פוסטים: ${counts["#total_posts"]?.toLocaleString() ?? 'N/A'}
    `;

    setTimeout(() => {
        container.style.opacity = "0";
        container.addEventListener("transitionend", container.remove);
    }, 5000);
});

//TODO: still needs improvement, but much better than before
onMatch("forumdisplay", "showForumStats", function() {
    function getDupeSortedDictionary(arr) {
        const counts = new Map();

        for (const item of arr) counts.set(item, (counts.get(item) || 0) + 1);

        const sortedArr = Array.from(counts, ([value, count]) => ({ value, count }));
        sortedArr.sort((a, b) => {
            return b.count === a.count ? 
                a.value.localeCompare(b.value) : b.count - a.count;
        });

        return sortedArr;
    }

    function openPopupWindow(title, content) {
        let dialog = document.getElementById("detailedStats");
        if (!dialog) {
            dialog = GM_addElement("dialog", {
                id: "detailedStats"
            })
            dialog.innerHTML = `${title}${content}`
            dialog.addEventListener("click", (e) => {
                if (e.target === dialog) dialog.close();
            });
        }

        dialog.showModal();
    }

    function removePopupWindow(id) {
        const dialog = document.getElementById(id);
        if (dialog && dialog.open) {
            dialog.close();
        }
    }

    const total = document.querySelectorAll('#threads .threadtitle').length;

    const container = document.createElement("div");
    container.id = "forumStatsContainer";
    const threadsList = document.querySelector(".threads_list_fxp");
    threadsList?.parentNode?.insertBefore(container, threadsList.nextSibling);

    const forumStats = GM_addElement(container, "div");
    GM_addElement(forumStats, "i", { textContent: `נתונים סטטיסטיים של ${total} אשכולות:` });
    const toArray = selector => Array.from(document.querySelectorAll(selector)).map(el => el.textContent);

    function appendLine(dict, introText, noText, suffix) {
        const line = GM_addElement(forumStats, "div");
        if (dict.length > 1 && dict[0].count > 1) {
            line.append(introText);
            for (let i = 0; i < dict.length && dict[i].count === dict[0].count; i++) {
                if (i > 0) line.append(" או ");
                GM_addElement(line, "b", { textContent: dict[i].value });
            }
            line.append(" עם " + dict[0].count + suffix);
        } else {
            line.append(noText);
        }
    }

    const publishersDict = getDupeSortedDictionary(toArray("#threads .threadinfo .username"));
    const commentorsDict = getDupeSortedDictionary(toArray("#threads .threadlastpost .username"));
    const prefixesDict = getDupeSortedDictionary(toArray("#threads .prefix").map(prefix => prefix.replace(/\||סקר: /g, '')));

    appendLine(
        publishersDict,
        "המפרסם הדומיננטי ביותר הוא ",
        "אין מפרסם דומיננטי במיוחד.",
        " אשכולות."
    );
    appendLine(
        commentorsDict,
        "המגיב האחרון הדומיננטי ביותר הוא ",
        "אין מגיב אחרון דומיננטי במיוחד.",
        " תגובות אחרונות."
    );
    appendLine( //TODO:  שנמצא ב-" + prefixesDict[0].count + " אשכולות.
        prefixesDict,
        "התיוג הנפוץ ביותר הוא ",
        "אין תיוג נפוץ במיוחד.",
        " אשכולות."
    );

    const parseNumber = text => parseInt(text.replace(/[^\d]/g, ""), 10) || 0;

    let commentsCount = 0;
    let viewsCount = 0;

    document.querySelectorAll("#threads .threadstats").forEach(el => {
        const [comments, views] = el.querySelectorAll("li");
        commentsCount += parseNumber(comments.textContent);
        viewsCount += parseNumber(views.textContent);
    });

    let viewsCommentsRatio = commentsCount > 0 ? Math.max(1, Math.round(viewsCount / commentsCount)) : "∞";

    const ratioLine = document.createElement("div");
    const b = document.createElement("b");
    b.textContent = viewsCommentsRatio + " צפיות";
    ratioLine.append("יחס הצפיות לתגובה הוא תגובה כל ", b, ".");
    forumStats.appendChild(ratioLine);

    const detailedStatsBtn = GM_addElement(forumStats, "div", { textContent: "+" });
    detailedStatsBtn.addEventListener("click", () => {
        const pContent = document.createElement("div");

        const flexTableContainer = GM_addElement(pContent, "div", {
            style: "display: flex; flexWrap: wrap;"
        });

        function helper(headerA, headerB, arr) {
            const table = GM_addElement(flexTableContainer, "table");
            const headerRow = GM_addElement(table, "tr")
            
            GM_addElement(headerRow, "th", { textContent: headerA });
            GM_addElement(headerRow, "th", { textContent: headerB });

            arr.forEach(item => {
                const tr = GM_addElement(table, "tr");
                GM_addElement(tr, "td", { textContent: item.value });
                GM_addElement(tr, "td", { textContent: item.count });
            });
        }

        helper("מפרסם", "אשכולות", publishersDict);
        helper("מגיב", "תגובות אחרונות", commentorsDict);
        helper("תיוג", "אשכולות", prefixesDict);

        const closeBtn = GM_addElement(pContent, "div", { textContent: "סגור" });
        closeBtn.addEventListener("click", () => removePopupWindow("detailedStats"));

        const forumTitle = document.querySelector('.lastnavbit > span').textContent;
        openPopupWindow(
            "סטטיסטיקות מפורטות לפורום " + forumTitle,
            pContent.outerHTML
        );
    });

    return () => {
        document.querySelectorAll("#forumStatsContainer, dialog").forEach(e => e.remove());
    };
});

onMatch("*", "nightMode", function () {
    const toggleDarkMode = (isEnabled) => setCookie("bb_darkmode",  isEnabled ? "1" : "0", 1440);

    function timeInMinutes(timeString) {
        if (!timeString) return 0;
        const [hours, minutes] = timeString.split(':').map(Number);
        return hours * 60 + minutes;
    }

    const darkModeThemeEl = document.querySelector("#darkmode_theme");

    function exec() {
        const now = new Date();
        const minutesCurrent = now.getHours() * 60 + now.getMinutes();

        const minutesStart = timeInMinutes(cfg.get("startTime"));
        const minutesEnd = timeInMinutes(cfg.get("endTime"));

        const rangeActive = minutesEnd < minutesStart
            ? (minutesCurrent >= minutesStart || minutesCurrent < minutesEnd)
            : (minutesCurrent >= minutesStart && minutesCurrent < minutesEnd);

        const nightModeActive = getCookie("bb_darkmode") == "1"

        if (nightModeActive && !rangeActive) {
            darkModeThemeEl?.classList?.remove('ofset');
            document.body.classList.remove('darkmode');
            document.querySelector('[href*="darkmode"]')?.remove();
            toggleDarkMode(false);
        } else if (!nightModeActive && rangeActive) {
            darkModeThemeEl?.classList?.add('ofset');
            document.body.classList.add('darkmode');
            if (!document.querySelector('[href*="darkmode"]')) {
                GM_addElement("link", {
                    rel: 'stylesheet',
                    href: '//static.fcdn.co.il/dyn/projects/css/desktop/darkmode.css'
                });
            }
            toggleDarkMode(true);
        }
    };

    exec();
    const interval = setInterval(exec, 20 * 1000);

    return () => clearInterval(interval);
});
onMatch("forumdisplay", "weeklyChallenge", async function() {
    const getTodayDMY = () => new Date().toLocaleDateString('en-GB').split('/').join('-');
    
    function parseDMY(dateStr) {
        const [day, month, year] = dateStr.split('-').map(Number);
        return new Date(year, month - 1, day);
    }

    function checkDateDifference(dateStr1, dateStr2) {
        const date1 = parseDMY(dateStr1);
        const date2 = parseDMY(dateStr2);
        return Math.abs(date2 - date1) / 604800000; // 604800000 = week
    }

    const target = document.querySelector(".flo > .description_clean");

    const container = GM_addElement(target, "div", {
        style: "direction: rtl; text-align: right; max-width: 300px; padding: 10px;"
    });

    GM_addElement(container, "h3", {
            textContent: "אשכול השבוע",
            style: "margin-top: 0;"
        });

    GM_addElement(container, "div", {
        id: "thread-week",
        textContent: "טוען...",
        style: "margin-bottom: 12px; color: #222;"
    });

    GM_addElement(container, "h3", {
        textContent: "משתמש השבוע"
    });

    GM_addElement(container, "div", {
        id: "member-week",
        textContent: "טוען...",
        style: "color: #222;"
    });

    const errorBox = GM_addElement(container, "div", {
        id: "weekly-error",
        style: "color: red; margin-top: 10px; display: none;"
    });

    const CACHE_KEY = "weeklyChallengeCache" + rawWindow.FORUM_ID_FXP;
    const cachedData = getCookie(CACHE_KEY);
    if (cachedData) {
        try {
            const { thread, member } = JSON.parse(cachedData);
            document.getElementById("thread-week").textContent = thread;
            document.getElementById("member-week").textContent = member;
            return;
        } catch (e) {}
    }

    const domParser = new DOMParser();

    const stickies = Array.from(document.querySelectorAll('.stickies .threadinfo'))
    .filter(node => /אשכול השבוע|משקיען השבוע|7 ימי ווינר|משקיען ואשכול/.test(node.textContent))
    .map(node => node.parentElement);
    if (stickies.length > 1 || stickies.length === 0) {
        container.style.display = "none";
        return; // maybe in the future, this will handle values greater than 1
    }

    const sticky = stickies.shift();
    const element = sticky.querySelector('a.lastpostdate');
    const url = element.href?.replace(/#post.*/, '');
    if (!url) {
        errorBox.style.display = "block";
        errorBox.innerHTML = 'האשכול השבועי לא מכיל אף הכרזה, יש לפנות ל<a href="https://www.fxp.co.il/forumdisplay.php?f=18" target="_blank">צוות תמיכה</a>';
        return;
    }
    const time = element.parentElement.textContent;
    const date = time.split(" ").shift().replace(/אתמול|היום/, getTodayDMY());
    if (checkDateDifference(getTodayDMY(), date) >= 2) {
        errorBox.style.display = "block";
        errorBox.innerHTML = 'האשכול השבועי לא עודכן זמן רב, יש לפנות ל<a href="https://www.fxp.co.il/forumdisplay.php?f=18" target="_blank">צוות תמיכה</a>';
        return;
    }
    const response = await fetcher(url + "&pp=1");
    const doc = domParser.parseFromString(response, "text/html");

    const threads = doc.querySelectorAll(".postcontent a[href*='showthread.php']");
    const members = doc.querySelectorAll(".postcontent a[href*='member.php']");
    /*
    TODO:
    - Handle multiple thread links properly current implementation only processes the first link and does not cover all cases.
    - Replace textContent below with real URL
    */
    let thread = threads.length > 0 ? threads[0].textContent.trim().replace(/^"|"$/g, "") : "לא נמצא אשכול";
    let member = members.length > 0 ? members[0].textContent.trim() : "לא נמצא משתמש";

    document.getElementById("thread-week").textContent = thread;
    document.getElementById("member-week").textContent = member;
    setCookie(CACHE_KEY, JSON.stringify({ thread, member }), 5);
})

// TODO: check that every editor is compatible 
// consider adding a check to validate the parameter
// currently does not support setting changes
// This code runs only once; executeCommand works better then
onMatch("show(post|thread)|newreply", "none", async function() {    
    const instances = await waitForObject("CKEDITOR.instances");
    const editor = Object.values(instances)[0];
    let content = editor.getData(); // document

    if (!content.trim()) {
        content = '\u200B';
    }
    
    ['size', 'font', 'color'].forEach(style => {
        const value = cfg.get(style);
        if (!value) return;
        content = `[${style}=${value}]${content}[/${style}]`;
    })

    editor.setData(content);
});

//temporally
onMatch("upload.php", 'none', function() {
    const user1 = [cfg.get("user1name"), cfg.get("user1pass")]
    if (user1.filter(Boolean).length) {
        const b = GM_addElement(document.querySelector(".back_image"), "button", {
            textContent: "התחבר ל-" + user1[0]
        })
        b.addEventListener("click", async function() {
            await login(...user1)
        });
    }
    const user2 = [cfg.get("user2name"), cfg.get("user2spass")]
    if (user2.filter(Boolean).length) {
        const b = GM_addElement(document.querySelector(".back_image"), "button", {
            textContent: "התחבר ל-" + user2[0]
        })
        b.addEventListener("click", async function() {
            await login(...user2)
        });
    }
});

async function login(vb_login_username, vb_login_password) {
    const postData = {
        method: "POST",
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        },
        body: new URLSearchParams({
            securitytoken: "guest",
            vb_login_username,
            vb_login_password,
            cookieuser: 1,
            do: "login"
        })
    };

    try {
        const data = await fetcher("https://www.fxp.co.il/login.php", postData);
        if (data.includes("התחברת בהצלחה")) alert("✅ Logged in successfully");
        else if (data.includes("במספר הפעמים המרבי")) alert("⚠️ Login restricted");
        alert("❌ Login failed: Invalid credentials");
    } catch (err) {
        alert("Login request failed");
    }
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址