ATW Custom Activity Feed

Automated background scanner for capturing ATW user posts that match someone's interests (based on keyword matching) from the site's activity feed. Provides an easy to use floating UI dashboard that allows 3-state multi-tag filtering, persistent storage, and JSON export/import.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         ATW Custom Activity Feed
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automated background scanner for capturing ATW user posts that match someone's interests (based on keyword matching) from the site's activity feed. Provides an easy to use floating UI dashboard that allows 3-state multi-tag filtering, persistent storage, and JSON export/import.
// @author       echo17
// @match        https://www.allthingsworn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // CONFIGURATION
    const CONFIG = {
        captureKeywords: ["🍋", "lemo", "bottle", "500ml", "cream", "pant", "thong"],
        excludeKeywords: [""],
        // notice: tags are for visual filtering, will only apply to posts already saved after matching against the capturekeywords.
        tags: {
            "Lemonade": ["lemo", "500ml", "250ml", "300ml"],
            "Panties": ["pant", "thong"],
            "Instant Content": ["Created Instant Content"],
            "Listing": ["Reposted a listing"],
            "Photo": ["Added a new photo"]
        },

        maxPages: 100,
        delayMs: 1500,
        cycleDelayMinutes: 10,
        baseUrl: '/activities?type=country&country=UK&page=1&sort=latest',
        storageKey: 'atw_feedscanner_v1'
    };

    let state = {
        isRunning: false,
        cycleTimer: null,
        currentPage: 1,
        currentUrl: CONFIG.baseUrl,
        seenSignatures: new Set(),
        blacklistedSignatures: new Set(),
        savedResults: []
    };

    let renderState = {
        filteredResults: [],
        currentlyRendered: 0,
        chunkSize: 50,
        customFilterText: '',
        sortOrder: 'desc',
        tagStates: {}
    };

    Object.keys(CONFIG.tags).forEach(tagName => {
        renderState.tagStates[tagName] = 'active';
    });

    function getMatchedTags(normalizedText) {
        let matches = [];
        for (const [tagName, keywords] of Object.entries(CONFIG.tags)) {
            if (keywords.some(kw => normalizedText.includes(kw.toLowerCase()))) {
                matches.push(tagName);
            }
        }
        return matches;
    }

    function saveState() {
        const stateToSave = {
            currentPage: state.currentPage,
            currentUrl: state.currentUrl,
            seenSignatures: Array.from(state.seenSignatures),
            blacklistedSignatures: Array.from(state.blacklistedSignatures),
            savedResults: state.savedResults
        };
        GM_setValue(CONFIG.storageKey, JSON.stringify(stateToSave));
    }

    function loadState() {
        const saved = GM_getValue(CONFIG.storageKey);
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                state.currentPage = parsed.currentPage || 1;
                state.currentUrl = parsed.currentUrl || CONFIG.baseUrl;
                state.seenSignatures = new Set(parsed.seenSignatures || []);
                state.blacklistedSignatures = new Set(parsed.blacklistedSignatures || []);
                state.savedResults = parsed.savedResults || [];

                state.savedResults.forEach(res => {
                    res.matchedTags = getMatchedTags(res.normalizedText);
                });
            } catch (e) {
                console.error("Failed to load scanner state:", e);
            }
        }
    }


    const ui = document.createElement('div');
    ui.id = 'scanner-monitor-ui';
    ui.innerHTML = `
        <style>
            #scanner-monitor-ui {
                position: fixed; top: 20px; right: 20px; width: 450px; height: 85vh;
                min-width: 320px; min-height: 250px;
                background: #fff; z-index: 999999; border-radius: 8px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.3); display: flex; flex-direction: column;
                font-family: Arial, sans-serif; border: 1px solid #ccc;
                resize: both; overflow: hidden;
            }
            #scanner-header {
                background: #2c3e50; color: #fff; padding: 12px 15px; margin: 0;
                font-size: 16px; font-weight: bold; display: flex; justify-content: space-between;
                cursor: move; user-select: none;
            }
            #scanner-controls {
                background: #ecf0f1; padding: 10px; display: flex; gap: 6px; flex-wrap: wrap;
                border-bottom: 1px solid #bdc3c7;
            }
            #scanner-controls button {
                padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer;
                background: #3498db; color: white; font-size: 12px; font-weight: bold; flex-grow: 1;
            }
            #scanner-controls button:hover { background: #2980b9; }
            #btn-clear { background: #e74c3c !important; }
            #btn-clear:hover { background: #c0392b !important; }
            #btn-export { background: #27ae60 !important; }
            #btn-export:hover { background: #2ecc71 !important; }
            #btn-import { background: #8e44ad !important; }
            #btn-import:hover { background: #9b59b6 !important; }

            #scanner-filters-container {
                padding: 10px; background: #fff; border-bottom: 1px solid #eee; display: flex; flex-direction: column; gap: 8px;
            }
            #scanner-tags-wrapper { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
            #scanner-tags { display: flex; flex-wrap: wrap; gap: 6px; flex-grow: 1; }

            .filter-tag {
                padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;
                cursor: pointer; transition: filter 0.2s, background 0.2s; border: 1px solid transparent; user-select: none;
            }
            .filter-tag:hover { filter: brightness(0.9); }
            .filter-tag.active { background: #e67e22 !important; color: white !important; border-color: #d35400; }
            .filter-tag.neutral { background: #e2e8f0 !important; color: #475569 !important; border-color: #cbd5e1; }
            .filter-tag.excluded { background: #e74c3c !important; color: white !important; border-color: #c0392b; }

            #btn-toggle-all-tags {
                background: #95a5a6; color: white; border: none; border-radius: 4px; padding: 3px 8px;
                font-size: 10px; font-weight: bold; cursor: pointer; transition: 0.2s; white-space: nowrap;
            }
            #btn-toggle-all-tags:hover { background: #7f8c8d; }

            .filter-row { display: flex; gap: 8px; }
            #custom-filter-input {
                flex-grow: 1; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; box-sizing: border-box;
            }
            #btn-sort { background: #7f8c8d !important; white-space: nowrap; }
            #btn-sort:hover { background: #95a5a6 !important; }

            #scanner-status {
                padding: 8px 10px; font-size: 12px; background: #f8f9fa; border-bottom: 1px solid #eee; color: #555; font-weight: bold;
            }
            #scanner-results { flex-grow: 1; overflow-y: auto; padding: 15px; background: #f9f9f9; position: relative; }
            .scanner-result-item {
                background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; margin-bottom: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);
            }
            .scanner-meta {
                display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px dashed #ddd; flex-wrap: wrap; gap: 5px;
            }
            .scanner-source-url { font-size: 11px; color: #3498db; text-decoration: none; word-break: break-all; }
            .scanner-source-url:hover { text-decoration: underline; }

            .scanner-meta-right { display: flex; align-items: center; gap: 6px; }
            .scanner-matched-tags { display: flex; gap: 4px; }
            .matched-tag { background: #f39c12; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; }
            .scanner-timestamp { font-size: 11px; color: #7f8c8d; font-weight: bold; background: #ecf0f1; padding: 2px 6px; border-radius: 4px; }

            .btn-delete-result {
                background: #e74c3c; color: white; border: none; border-radius: 4px; padding: 2px 6px; font-size: 10px; font-weight: bold; cursor: pointer; transition: 0.2s;
            }
            .btn-delete-result:hover { background: #c0392b; }

            .scanner-result-item .activity-item { margin: 0 !important; width: 100%; }
            .scanner-result-item img { max-width: 100%; height: auto; }
            .scanner-result-item img.main, .scanner-result-item .user-circle img { aspect-ratio: 1 / 1 !important; object-fit: cover !important; border-radius: 50% !important; }
            .scanner-result-item img.country-flag { width: 18px !important; height: 18px !important; aspect-ratio: 1 / 1 !important; object-fit: cover !important; border-radius: 50% !important; display: inline-block; vertical-align: middle; }
            #scroll-sentinel { height: 20px; width: 100%; }
        </style>

        <div id="scanner-header">
            <span>Keyword Scanner</span>
            <span id="scanner-count">0 matches</span>
        </div>
        <div id="scanner-controls">
            <button id="btn-toggle">Start</button>
            <button id="btn-restart">Restart</button>
            <button id="btn-clear" class="danger" title="Clear History (Keeps Blacklist)">Clear</button>
            <button id="btn-export">Export</button>
            <button id="btn-import">Import</button>
        </div>
        <div id="scanner-filters-container">
            <div id="scanner-tags-wrapper">
                <div id="scanner-tags"></div>
                <button id="btn-toggle-all-tags">All: Active</button>
            </div>
            <div class="filter-row">
                <input type="text" id="custom-filter-input" placeholder="Type to filter...">
                <button id="btn-sort">Sort: Newest</button>
            </div>
        </div>
        <div id="scanner-status">Status: Idle</div>
        <div id="scanner-results">
            <div id="scroll-sentinel"></div>
        </div>
    `;
    document.body.appendChild(ui);

    const header = document.getElementById('scanner-header');
    let isDragging = false, startX, startY, startLeft, startTop;

    header.addEventListener('mousedown', (e) => {
        isDragging = true; startX = e.clientX; startY = e.clientY;
        const rect = ui.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top;
        ui.style.right = 'auto'; ui.style.bottom = 'auto'; e.preventDefault();
    });
    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        ui.style.left = `${startLeft + e.clientX - startX}px`; ui.style.top = `${startTop + e.clientY - startY}px`;
    });
    document.addEventListener('mouseup', () => { isDragging = false; });


    const btnToggle = document.getElementById('btn-toggle');
    const btnRestart = document.getElementById('btn-restart');
    const btnClear = document.getElementById('btn-clear');
    const btnExport = document.getElementById('btn-export');
    const btnImport = document.getElementById('btn-import');
    const btnSort = document.getElementById('btn-sort');
    const btnToggleAllTags = document.getElementById('btn-toggle-all-tags');
    const statusEl = document.getElementById('scanner-status');
    const resultsEl = document.getElementById('scanner-results');
    const countEl = document.getElementById('scanner-count');
    const tagsContainer = document.getElementById('scanner-tags');
    const filterInput = document.getElementById('custom-filter-input');
    const sentinel = document.getElementById('scroll-sentinel');

    Object.keys(CONFIG.tags).forEach(tagName => {
        const span = document.createElement('span');
        span.className = 'filter-tag active';
        span.dataset.tag = tagName;
        span.textContent = '✓ ' + tagName;

        span.addEventListener('click', () => {
            const current = renderState.tagStates[tagName];
            let nextState;

            if (current === 'active') {
                nextState = 'neutral';
                span.className = 'filter-tag neutral';
                span.textContent = tagName;
            } else if (current === 'neutral') {
                nextState = 'excluded';
                span.className = 'filter-tag excluded';
                span.textContent = '✖ ' + tagName;
            } else {
                nextState = 'active';
                span.className = 'filter-tag active';
                span.textContent = '✓ ' + tagName;
            }

            renderState.tagStates[tagName] = nextState;
            applyFilters();
        });
        tagsContainer.appendChild(span);
    });

    btnToggleAllTags.addEventListener('click', () => {
        const states = Object.values(renderState.tagStates);
        const allActive = states.every(s => s === 'active');
        const allNeutral = states.every(s => s === 'neutral');

        let nextState = 'active';
        if (allActive) nextState = 'neutral';
        else if (allNeutral) nextState = 'excluded';
        else nextState = 'active';

        const labels = { 'active': 'All: Active', 'neutral': 'All: Neutral', 'excluded': 'All: Excluded' };
        btnToggleAllTags.textContent = labels[nextState];

        document.querySelectorAll('.filter-tag').forEach(span => {
            span.className = `filter-tag ${nextState}`;
            const tName = span.dataset.tag;
            if (nextState === 'active') span.textContent = '✓ ' + tName;
            else if (nextState === 'excluded') span.textContent = '✖ ' + tName;
            else span.textContent = tName;

            renderState.tagStates[tName] = nextState;
        });
        applyFilters();
    });

    resultsEl.addEventListener('click', (e) => {
        if (e.target.classList.contains('btn-delete-result')) {
            const rawSignature = decodeURIComponent(e.target.getAttribute('data-sig'));
            state.blacklistedSignatures.add(rawSignature);
            state.savedResults = state.savedResults.filter(res => res.normalizedText !== rawSignature);
            saveState(); applyFilters(); updateStatus('Item deleted and blacklisted.');
        }
    });


    btnExport.addEventListener('click', () => {
        const exportData = {
            currentPage: state.currentPage, currentUrl: state.currentUrl,
            seenSignatures: Array.from(state.seenSignatures),
            blacklistedSignatures: Array.from(state.blacklistedSignatures),
            savedResults: state.savedResults
        };
        const dataStr = JSON.stringify(exportData);
        const blob = new Blob([dataStr], { type: "application/json" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); a.href = url;
        a.download = `Scanner_Backup_${new Date().toISOString().split('T')[0]}.json`;
        document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
        updateStatus('Data exported successfully.');
    });

    btnImport.addEventListener('click', () => {
        const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json';
        input.onchange = e => {
            const file = e.target.files[0]; if (!file) return;
            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    const importedState = JSON.parse(event.target.result);
                    if (importedState.savedResults && Array.isArray(importedState.seenSignatures)) {
                        state.currentPage = importedState.currentPage || 1;
                        state.currentUrl = importedState.currentUrl || CONFIG.baseUrl;
                        state.seenSignatures = new Set(importedState.seenSignatures);
                        state.blacklistedSignatures = new Set(importedState.blacklistedSignatures || []);

                        importedState.savedResults.forEach(res => {
                            res.matchedTags = getMatchedTags(res.normalizedText);
                        });
                        state.savedResults = importedState.savedResults;

                        btnToggleAllTags.textContent = 'All: Active';
                        document.querySelectorAll('.filter-tag').forEach(span => {
                            span.className = 'filter-tag active';
                            span.textContent = '✓ ' + span.dataset.tag;
                            renderState.tagStates[span.dataset.tag] = 'active';
                        });
                        filterInput.value = ''; renderState.customFilterText = '';

                        saveState(); applyFilters(); updateStatus(`Imported ${state.savedResults.length} items.`);
                    } else { alert("Error: Invalid backup file format."); }
                } catch (err) { alert("Error reading file."); }
            };
            reader.readAsText(file);
        };
        input.click();
    });


    filterInput.addEventListener('input', (e) => {
        renderState.customFilterText = e.target.value.toLowerCase();
        applyFilters();
    });

    btnSort.addEventListener('click', () => {
        if (renderState.sortOrder === 'desc') {
            renderState.sortOrder = 'asc'; btnSort.textContent = 'Sort: Oldest';
        } else {
            renderState.sortOrder = 'desc'; btnSort.textContent = 'Sort: Newest';
        }
        applyFilters();
    });

    function applyFilters() {
        const txtFilter = renderState.customFilterText;
        const activeTags = Object.keys(renderState.tagStates).filter(k => renderState.tagStates[k] === 'active');

        renderState.filteredResults = state.savedResults.filter(res => {
            if (txtFilter && !res.normalizedText.includes(txtFilter)) return false;

            if (!res.matchedTags || res.matchedTags.length === 0) {
                return activeTags.length === 0;
            }

            const hasExcluded = res.matchedTags.some(t => renderState.tagStates[t] === 'excluded');
            if (hasExcluded) return false;

            if (activeTags.length > 0) {
                const hasActive = res.matchedTags.some(t => renderState.tagStates[t] === 'active');
                if (!hasActive) return false;
            }

            return true;
        });

        renderState.filteredResults.sort((a, b) => {
            return renderState.sortOrder === 'desc' ? b.timestampMs - a.timestampMs : a.timestampMs - b.timestampMs;
        });

        Array.from(resultsEl.children).forEach(child => { if (child.id !== 'scroll-sentinel') child.remove(); });
        renderState.currentlyRendered = 0;
        countEl.textContent = `${renderState.filteredResults.length} match${renderState.filteredResults.length === 1 ? '' : 'es'}`;
        renderNextChunk();
    }

    function renderNextChunk() {
        if (renderState.currentlyRendered >= renderState.filteredResults.length) {
            sentinel.style.display = 'none'; return;
        }
        sentinel.style.display = 'block';
        const fragment = document.createDocumentFragment();
        const chunk = renderState.filteredResults.slice(renderState.currentlyRendered, renderState.currentlyRendered + renderState.chunkSize);

        chunk.forEach(res => {
            const wrapper = document.createElement('div'); wrapper.className = 'scanner-result-item';

            let tagsHtml = res.matchedTags && res.matchedTags.length > 0
            ? `<div class="scanner-matched-tags">${res.matchedTags.map(tag => `<span class="matched-tag">${tag}</span>`).join('')}</div>`
                : '';

            wrapper.innerHTML = `
                <div class="scanner-meta">
                    <a class="scanner-source-url" href="${res.sourceUrl}" target="_blank">View Source Page</a>
                    <div class="scanner-meta-right">
                        ${tagsHtml}
                        <span class="scanner-timestamp">${res.timestampText}</span>
                        <button class="btn-delete-result" data-sig="${encodeURIComponent(res.normalizedText)}" title="Delete and Blacklist">✖</button>
                    </div>
                </div>
                <div>${res.html}</div>`;
            fragment.appendChild(wrapper);
        });
        resultsEl.insertBefore(fragment, sentinel);
        renderState.currentlyRendered += chunk.length;
    }

    const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) renderNextChunk();
    }, { root: resultsEl, rootMargin: '100px' });
    observer.observe(sentinel);


    const sleep = ms => new Promise(r => setTimeout(r, ms));
    function updateStatus(text) { statusEl.textContent = `Status: ${text}`; }

    function getNormalizedText(domElement) {
        let clone = domElement.cloneNode(true);
        clone.querySelectorAll('.mt-1.mb-0.small, .activity-comments').forEach(el => el.remove());
        let attributeTexts = [];
        clone.querySelectorAll('[alt], [title]').forEach(el => {
            if (el.alt) attributeTexts.push(el.alt); if (el.title) attributeTexts.push(el.title);
        });
        let textWithoutTags = clone.innerHTML.replace(/<[^>]+>/g, ' ');
        let textArea = document.createElement("textarea");
        textArea.innerHTML = textWithoutTags + " " + attributeTexts.join(" ");
        return textArea.value.toLowerCase().replace(/[\u200B-\u200D\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
    }

    function extractTimeFromHtml(domElement) {
        const clockIcon = domElement.querySelector('.fa-clock');
        if (clockIcon && clockIcon.parentElement) return (clockIcon.parentElement.textContent || "").replace(/[\n\r\t]/g, '').trim();
        return null;
    }

    function calculateAbsoluteTime(relativeStr, fetchDate) {
        if (!relativeStr) return { text: "Unknown time", ms: 0 };
        const match = relativeStr.trim().match(/^(\d+)([smhdw])$/i);
        const absoluteDate = new Date(fetchDate.getTime());
        if (match) {
            const val = parseInt(match[1], 10); const unit = match[2].toLowerCase();
            if (unit === 's') absoluteDate.setSeconds(absoluteDate.getSeconds() - val);
            else if (unit === 'm') absoluteDate.setMinutes(absoluteDate.getMinutes() - val);
            else if (unit === 'h') absoluteDate.setHours(absoluteDate.getHours() - val);
            else if (unit === 'd') absoluteDate.setDate(absoluteDate.getDate() - val);
            else if (unit === 'w') absoluteDate.setDate(absoluteDate.getDate() - (val * 7));
        }
        return {
            text: absoluteDate.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute:'2-digit' }),
            ms: absoluteDate.getTime()
        };
    }

    function lockUrlWithStamp(originalUrl, nextPageUrl) {
        if (originalUrl.includes('stamp=')) return originalUrl;
        if (!nextPageUrl) return originalUrl;
        try {
            const nextUrlObj = new URL(nextPageUrl, window.location.origin);
            const stamp = nextUrlObj.searchParams.get('stamp');
            if (stamp) {
                const lockedObj = new URL(originalUrl, window.location.origin);
                lockedObj.searchParams.set('stamp', stamp);
                return lockedObj.toString();
            }
        } catch (e) {}
        return originalUrl;
    }

    async function processPage(pageUrl) {
        try {
            const fetchDate = new Date();
            const response = await fetch(pageUrl); const html = await response.text();
            const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html');
            const items = doc.querySelectorAll('.activity-item');

            let discoveredNextUrl = null;
            const moreBtn = doc.querySelector('a.btn.btn-block.btn-primary.click-loading');
            if (moreBtn && moreBtn.href) discoveredNextUrl = moreBtn.href;

            const secureSourceUrl = lockUrlWithStamp(pageUrl, discoveredNextUrl);
            let newlyFoundCount = 0;

            items.forEach((item) => {
                const normalizedText = getNormalizedText(item);

                if (state.seenSignatures.has(normalizedText) || state.blacklistedSignatures.has(normalizedText)) return;

                const isExcluded = CONFIG.excludeKeywords.some(ex => normalizedText.includes(ex.toLowerCase()));
                if (isExcluded) {
                    state.seenSignatures.add(normalizedText);
                    return;
                }

                const isCaptured = CONFIG.captureKeywords.some(cap => normalizedText.includes(cap.toLowerCase()));
                if (!isCaptured) {
                    state.seenSignatures.add(normalizedText);
                    return;
                }

                const matchedTags = getMatchedTags(normalizedText);
                const relativeTimeStr = extractTimeFromHtml(item);
                const timeData = calculateAbsoluteTime(relativeTimeStr, fetchDate);

                const resultData = {
                    html: item.outerHTML,
                    sourceUrl: secureSourceUrl,
                    timestampText: timeData.text,
                    timestampMs: timeData.ms,
                    normalizedText: normalizedText,
                    matchedTags: matchedTags
                };

                state.seenSignatures.add(normalizedText);
                state.savedResults.push(resultData);
                newlyFoundCount++;
            });

            if (newlyFoundCount > 0) { saveState(); applyFilters(); }
            return discoveredNextUrl;
        } catch (error) { console.error("Scanner Error fetching page:", error); return null; }
    }

    async function runScanner() {
        if (state.cycleTimer) {
            clearInterval(state.cycleTimer);
            state.cycleTimer = null;
        }

        if (state.isRunning && state.currentPage > CONFIG.maxPages) {
            state.currentPage = 1;
            state.currentUrl = CONFIG.baseUrl;
        }

        state.isRunning = true;
        btnToggle.textContent = 'Pause';

        while (state.isRunning && state.currentPage <= CONFIG.maxPages) {
            updateStatus(`Scanning page ${state.currentPage} of ${CONFIG.maxPages}...`);
            const discoveredNextUrl = await processPage(state.currentUrl);
            if (!state.isRunning) break;

            if (!discoveredNextUrl) {
                updateStatus('Feed ended or structure changed. Pausing.');
                state.isRunning = false;
                break;
            }

            state.currentUrl = discoveredNextUrl;
            state.currentPage++;
            saveState();

            if (state.currentPage <= CONFIG.maxPages) {
                updateStatus(`Waiting ${CONFIG.delayMs}ms...`);
                await sleep(CONFIG.delayMs);
            }
        }

        // --- THE AUTOPILOT LOOP ---
        if (state.isRunning && state.currentPage > CONFIG.maxPages) {
            state.currentPage = 1;
            state.currentUrl = CONFIG.baseUrl;
            saveState();

            let countdown = CONFIG.cycleDelayMinutes * 60;
            updateStatus(`Sleeping. Next cycle in ${countdown}s...`);

            state.cycleTimer = setInterval(() => {
                countdown--;
                if (countdown <= 0) {
                    clearInterval(state.cycleTimer);
                    state.cycleTimer = null;
                    if (state.isRunning) runScanner();
                } else {
                    updateStatus(`Sleeping. Next cycle in ${countdown}s...`);
                }
            }, 1000);
        } else if (!state.isRunning) {
            btnToggle.textContent = 'Start';
            saveState();
        }
    }


    // initisalising
    loadState(); applyFilters();
    if (state.savedResults.length > 0) updateStatus(`Idle. Restored page ${state.currentPage}.`);

    btnToggle.addEventListener('click', () => {
        if (state.isRunning) {
            state.isRunning = false;
            if (state.cycleTimer) {
                clearInterval(state.cycleTimer);
                state.cycleTimer = null;
            }
            btnToggle.textContent = 'Resume';
            updateStatus('Paused.');
            saveState();
        }
        else {
            runScanner();
        }
    });

    btnRestart.addEventListener('click', () => {
        state.isRunning = false;
        if (state.cycleTimer) {
            clearInterval(state.cycleTimer);
            state.cycleTimer = null;
        }
        state.currentPage = 1;
        state.currentUrl = CONFIG.baseUrl;
        updateStatus('Cycle restarted to Page 1. History preserved.');
        btnToggle.textContent = 'Start';
        saveState();
        setTimeout(runScanner, 100);
    });

    btnClear.addEventListener('click', () => {
        if(confirm("Are you sure you want to clear your saved results? (Note: Your manual blacklist will be saved so deleted items won't return)")) {
            state.isRunning = false;
            if (state.cycleTimer) {
                clearInterval(state.cycleTimer);
                state.cycleTimer = null;
            }
            state.currentPage = 1;
            state.currentUrl = CONFIG.baseUrl;
            state.seenSignatures.clear();
            state.savedResults = [];

            btnToggleAllTags.textContent = 'All: Active';
            document.querySelectorAll('.filter-tag').forEach(span => {
                span.className = 'filter-tag active';
                span.textContent = '✓ ' + span.dataset.tag;
                renderState.tagStates[span.dataset.tag] = 'active';
            });
            filterInput.value = ''; renderState.customFilterText = '';

            applyFilters();
            updateStatus('Results cleared. Blacklist preserved.');
            btnToggle.textContent = 'Start';
            saveState();
        }
    });

})();