Torn: Activity Log filter, export & reverse

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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);
    }
}