Torn: Activity Log filter, export & reverse

Export only visually filtered items and reverse the layout view on the Torn Log Page

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn: Activity Log filter, export & reverse
// @namespace    lugburz.activity_log_filter_export
// @version      0.6.0
// @description  Export only visually filtered items and reverse the layout view on the Torn Log Page
// @author       Lugburz & Community
// @match        https://www.torn.com/*
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// ==/UserScript==

var exportAll = false;
var logpageLogs = [];

function addInterceptor() {
    if (unsafeWindow.__logInterceptorAdded) return;
    unsafeWindow.__logInterceptorAdded = true;

    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async function(...args) {
        const response = await originalFetch.apply(this, args);
        const url = args[0] ? (typeof args[0] === 'string' ? args[0] : args[0].url) : '';

        if (url && url.indexOf('page.php?sid=activityLogUserData') > -1) {
            try {
                const text = await response.clone().text();
                const json = JSON.parse(text);
                if (json && json.log) {
                    for (const l of json.log) {
                        if (!logpageLogs.some(existing => existing.ID === l.ID)) {
                            logpageLogs.push(l);
                        }
                    }
                    setTimeout(addScriptButtons, 300);
                }
            } catch (e) { console.error(e); }
        }
        return response;
    };
}

function downloadCsv(csv) {
    const myblob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
    const myurl = window.URL.createObjectURL(myblob);
    const ancorTag = document.createElement('a');
    ancorTag.href = myurl;
    ancorTag.download = 'filtered_activity_log.csv';
    document.body.appendChild(ancorTag);
    ancorTag.click();
    document.body.removeChild(ancorTag);
}

function scrapeVisiblePageLogs() {
    if (typeof $ === 'undefined') return '';
    let csv = 'time,category,text\n';

    const logRows = $('#activity-log-root').find('tr, [class*=logRow___], [class*=row___]');

    logRows.each((index, el) => {
        if ($(el).is(':hidden') || $(el).attr('class')?.includes('title')) return;

        const timeEl = $(el).find('td[class^=time], [class*=time___]');
        const textEl = $(el).find('span.log-text, [class*=text___]');

        if (textEl.length > 0) {
            let id = textEl.attr('id') ? textEl.attr('id').replace('text-', '') : null;
            let category = "Filtered Log";

            if (id) {
                const networkMatch = logpageLogs.find(log => log.ID === id);
                if (networkMatch) {
                    category = networkMatch.category;
                }
            }

            const timeText = timeEl.text().trim() || "N/A";
            const logText = textEl.text().trim().replace(/\n/g, ' ').replace(/"/g, '""');

            csv += [`"${timeText}"`, category, `"${logText}"`].join(',') + '\n';
        }
    });
    return csv;
}

function exportActivityLog() {
    if (!exportAll) {
        const scrapedCsv = scrapeVisiblePageLogs();
        const rowCount = scrapedCsv.split('\n').length - 2;

        if (rowCount > 0) {
            downloadCsv(scrapedCsv);
        } else {
            alert("No visible logs found. Please make sure you are on the Log page and logs are currently displayed.");
        }
        return;
    }

    if (logpageLogs.length === 0) {
        alert("Please wait for the Log Page data to finish loading.");
        return;
    }
    let csv = 'time,category,text\n';
    for (const l of logpageLogs) {
        const time = new Date(l.time * 1000);
        csv += [`"${time.toUTCString()}"`, l.category, `"${l.text.replace(/\n/g, ' ').replace(/"/g, '""')}"`].join(',') + '\n';
    }
    downloadCsv(csv);
}

function reverseLogView() {
    if (typeof $ === 'undefined') return;

    // Find the container holding the rows (works for both old tables and modern list dividers)
    const container = $('#activity-log-root').find('tbody, [class*=logWrapper___]');

    if (container.length > 0) {
        const rows = container.children('tr, [class*=logRow___], [class*=row___]').not('[class*=title]');
        container.append(rows.get().reverse());
    }
}

function addScriptButtons() {
    if (typeof $ === 'undefined' || $('#exportLogsBtn').length > 0) return;

    let buttonClass = 'button___37oQ0';
    let wrapper = $('#activity-log-root').find('[class*=buttonsWrapper], [class*=filterWrapper], [class*=container___]');

    if (!wrapper.length) {
        wrapper = $('#activity-log-root').find('h4').first();
    }

    // Export Button
    const exportBtn = `<button id="exportLogsBtn" class="${buttonClass}" style="line-height: 24px; padding: 0 12px; background-color: var(--default-blue-color, #337ab7); color: #fff; border: none; border-radius: 4px; margin-left: 10px; cursor: pointer;" title="Export filtered logs as CSV" type="button">Export Filtered CSV</button>`;

    // Reverse Button
    const reverseBtn = `<button id="reverseLogsBtn" class="${buttonClass}" style="line-height: 24px; padding: 0 12px; background-color: #4caf50; color: #fff; border: none; border-radius: 4px; margin-left: 10px; cursor: pointer;" title="Reverse log order layout" type="button">Reverse View</button>`;

    wrapper.append(exportBtn);
    wrapper.append(reverseBtn);

    $('#exportLogsBtn').on('click', () => {
        exportActivityLog();
    });

    $('#reverseLogsBtn').on('click', () => {
        reverseLogView();
    });
}

addInterceptor();

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
} else {
    init();
}

function init() {
    if (typeof $ !== 'undefined') {
        GM_registerMenuCommand('Export Filtered Logs', exportActivityLog);
        GM_registerMenuCommand('Reverse Log View', reverseLogView);

        setInterval(() => {
            if ($('#activity-log-root').length && !$('#exportLogsBtn').length) {
                addScriptButtons();
            }
        }, 1000);
    } else {
        setTimeout(init, 300);
    }
}