☰

Reddit Saved - Session Links Collector + Export

Load your actual Reddit Saved posts directly from Reddit's Saved listing, then review, copy or export them as TXT/CSV. Does not scrape page DOM or collect ordinary Reddit activity.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Reddit Saved - Session Links Collector + Export
// @namespace    https://greasyfork.org/userscripts/thochaos
// @version      2026.10.04.4
// @description  Load your actual Reddit Saved posts directly from Reddit's Saved listing, then review, copy or export them as TXT/CSV. Does not scrape page DOM or collect ordinary Reddit activity.
// @author       thochaos
// @license      MIT
// @match        https://reddit.com/*
// @match        https://*.reddit.com/*
// @noframes
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const VERSION =
        '2026.10.04.4';

    /*
     * New key deliberately prevents contaminated DOM-collected
     * data from v2026.10.04.1-.3 being restored.
     */
    const STORAGE_KEY =
        'redditSavedLinksCollector_session_v3';

    const ROUTE_POLL_MS =
        500;

    const PAGE_LIMIT =
        100;

    let linksOnlyActive =
        false;

    let collectorActive =
        false;

    let loading =
        false;

    let currentAbortController =
        null;

    let lastKnownUrl =
        location.href;

    let routeSyncTimer =
        null;

    let toastTimer =
        null;

    const collectedPosts =
        new Map();

    // ============================================================
    // Helpers
    // ============================================================

    function normalizeWhitespace(value) {
        return String(
            value || ''
        )
            .replace(/\s+/g, ' ')
            .trim();
    }

    function escapeHtml(value) {
        return String(value)
            .replaceAll(
                '&',
                '&'
            )
            .replaceAll(
                '<',
                '&lt;'
            )
            .replaceAll(
                '>',
                '&gt;'
            )
            .replaceAll(
                '"',
                '&quot;'
            )
            .replaceAll(
                "'",
                '&#039;'
            );
    }

    function escapeCSV(value) {
        const str =
            String(
                value ?? ''
            );

        if (
            str.includes(',') ||
            str.includes('"') ||
            str.includes('\n') ||
            str.includes('\r')
        ) {
            return (
                '"' +
                str.replaceAll(
                    '"',
                    '""'
                ) +
                '"'
            );
        }

        return str;
    }

    function getDateString() {
        return new Date()
            .toISOString()
            .slice(
                0,
                10
            );
    }

    function sleep(ms) {
        return new Promise(
            resolve =>
                setTimeout(
                    resolve,
                    ms
                )
        );
    }

    // ============================================================
    // Saved route
    // ============================================================

    function getSavedRoute() {
        const path =
            location.pathname
                .replace(
                    /\/+$/,
                    ''
                );

        const match =
            path.match(
                /^\/user\/([^/]+)\/saved$/i
            );

        if (!match) {
            return null;
        }

        let username;

        try {
            username =
                decodeURIComponent(
                    match[1]
                );
        } catch {
            username =
                match[1];
        }

        return {
            username,
            path
        };
    }

    function isSavedPage() {
        return Boolean(
            getSavedRoute()
        );
    }

    // ============================================================
    // Reddit post URLs
    // ============================================================

    function normalizePermalink(
        permalink
    ) {
        if (!permalink) {
            return '';
        }

        try {
            const parsed =
                new URL(
                    permalink,
                    'https://www.reddit.com'
                );

            if (
                !/\/comments\/[a-z0-9]+/i.test(
                    parsed.pathname
                )
            ) {
                return '';
            }

            parsed.protocol =
                'https:';

            parsed.hostname =
                'www.reddit.com';

            parsed.search =
                '';

            parsed.hash =
                '';

            let path =
                parsed.pathname;

            if (
                !path.endsWith('/')
            ) {
                path += '/';
            }

            return (
                'https://www.reddit.com' +
                path
            );

        } catch {
            return '';
        }
    }

    // ============================================================
    // Session persistence
    // ============================================================

    function loadStoredPosts() {
        try {
            const raw =
                JSON.parse(
                    sessionStorage.getItem(
                        STORAGE_KEY
                    ) ||
                    '[]'
                );

            if (
                !Array.isArray(
                    raw
                )
            ) {
                return;
            }

            collectedPosts.clear();

            for (
                const post
                of raw
            ) {
                if (
                    !post?.url
                ) {
                    continue;
                }

                collectedPosts.set(
                    post.url,
                    post
                );
            }

        } catch (error) {
            console.warn(
                '[Reddit Saved Collector] Could not restore session.',
                error
            );
        }
    }

    function saveStoredPosts() {
        try {
            sessionStorage.setItem(
                STORAGE_KEY,
                JSON.stringify(
                    [
                        ...collectedPosts.values()
                    ]
                )
            );

        } catch (error) {
            console.warn(
                '[Reddit Saved Collector] Could not save session.',
                error
            );
        }
    }

    // ============================================================
    // Reddit Saved JSON API
    // ============================================================

    function buildSavedListingUrl(
        username,
        after,
        count
    ) {
        const encodedUser =
            encodeURIComponent(
                username
            );

        const url =
            new URL(
                `/user/${encodedUser}/saved.json`,
                location.origin
            );

        url.searchParams.set(
            'limit',
            String(
                PAGE_LIMIT
            )
        );

        /*
         * Critical:
         *
         * Only saved Link objects (posts).
         * Saved comments are deliberately excluded.
         */
        url.searchParams.set(
            'type',
            'links'
        );

        url.searchParams.set(
            'raw_json',
            '1'
        );

        url.searchParams.set(
            'count',
            String(
                count
            )
        );

        if (after) {
            url.searchParams.set(
                'after',
                after
            );
        }

        return url.href;
    }

    function listingChildToPost(
        child,
        existingPosts
    ) {
        /*
         * Reddit fullname kinds:
         *
         * t3 = Link/post
         * t1 = Comment
         *
         * type=links should already limit the response to t3,
         * but enforce it again here.
         */
        if (
            child?.kind !==
            't3'
        ) {
            return null;
        }

        const data =
            child.data;

        if (!data) {
            return null;
        }

        const url =
            normalizePermalink(
                data.permalink
            );

        if (!url) {
            return null;
        }

        const previous =
            existingPosts.get(
                url
            );

        return {
            id:
                data.name ||
                (
                    data.id
                        ? `t3_${data.id}`
                        : ''
                ),

            url,

            title:
                normalizeWhitespace(
                    data.title
                ) ||
                url,

            subreddit:
                normalizeWhitespace(
                    data.subreddit
                ),

            author:
                normalizeWhitespace(
                    data.author
                ),

            sourceTitle:
                'Reddit Saved',

            sourceUrl:
                location.href,

            firstSeen:
                previous?.firstSeen ||
                new Date()
                    .toISOString()
        };
    }

    async function requestSavedPage(
        username,
        after,
        count,
        signal
    ) {
        const url =
            buildSavedListingUrl(
                username,
                after,
                count
            );

        console.debug(
            '[Reddit Saved Collector] Requesting Saved listing:',
            {
                username,
                after,
                count,
                url
            }
        );

        const response =
            await fetch(
                url,
                {
                    method:
                        'GET',

                    credentials:
                        'include',

                    headers: {
                        Accept:
                            'application/json'
                    },

                    signal
                }
            );

        if (
            response.status ===
            401 ||
            response.status ===
            403
        ) {
            throw new Error(
                'Reddit rejected access to the Saved listing. Make sure you are logged in.'
            );
        }

        if (!response.ok) {
            throw new Error(
                `Reddit Saved listing returned HTTP ${response.status}.`
            );
        }

        const contentType =
            response.headers
                .get(
                    'content-type'
                ) ||
            '';

        if (
            !contentType
                .toLowerCase()
                .includes(
                    'json'
                )
        ) {
            throw new Error(
                'Reddit returned a non-JSON response for the Saved listing.'
            );
        }

        let payload;

        try {
            payload =
                await response.json();

        } catch {
            throw new Error(
                'Reddit returned invalid JSON for the Saved listing.'
            );
        }

        const listing =
            payload?.data;

        if (
            !listing ||
            !Array.isArray(
                listing.children
            )
        ) {
            throw new Error(
                'Reddit returned an unexpected Saved listing format.'
            );
        }

        return listing;
    }

    async function loadSavedPosts() {
        const route =
            getSavedRoute();

        if (!route) {
            return;
        }

        if (loading) {
            return;
        }

        loading =
            true;

        updateButtons();

        if (
            currentAbortController
        ) {
            currentAbortController.abort();
        }

        const controller =
            new AbortController();

        currentAbortController =
            controller;

        const existingPosts =
            new Map(
                collectedPosts
            );

        /*
         * Build the refreshed result separately.
         *
         * We only replace the current collection after Reddit has
         * completed pagination successfully. A failed request therefore
         * cannot destroy the last good collection.
         */
        const refreshed =
            new Map();

        const seenCursors =
            new Set();

        let after =
            null;

        let count =
            0;

        let page =
            0;

        try {
            setStatus(
                'Loading saved posts from Reddit...'
            );

            while (true) {
                /*
                 * If we navigated away while a request was in flight,
                 * stop immediately.
                 */
                if (!isSavedPage()) {
                    throw new DOMException(
                        'Navigation changed',
                        'AbortError'
                    );
                }

                page++;

                if (
                    page >
                    100
                ) {
                    throw new Error(
                        'Pagination safety limit reached.'
                    );
                }

                setStatus(
                    `Loading Saved page ${page} — ${refreshed.size} posts found...`
                );

                const listing =
                    await requestSavedPage(
                        route.username,
                        after,
                        count,
                        controller.signal
                    );

                const children =
                    listing.children;

                let postsThisPage =
                    0;

                for (
                    const child
                    of children
                ) {
                    const post =
                        listingChildToPost(
                            child,
                            existingPosts
                        );

                    if (!post) {
                        continue;
                    }

                    refreshed.set(
                        post.url,
                        post
                    );

                    postsThisPage++;
                }

                count +=
                    children.length;

                console.debug(
                    '[Reddit Saved Collector] Saved listing page complete:',
                    {
                        page,
                        listingItems:
                            children.length,

                        postsAccepted:
                            postsThisPage,

                        totalSavedPosts:
                            refreshed.size,

                        nextAfter:
                            listing.after
                    }
                );

                /*
                 * Do NOT use:
                 *
                 * children.length < PAGE_LIMIT
                 *
                 * as the stopping rule.
                 *
                 * Reddit listings can return short intermediate pages.
                 * The authoritative end condition is after === null.
                 */
                const nextAfter =
                    listing.after ||
                    null;

                if (!nextAfter) {
                    break;
                }

                if (
                    seenCursors.has(
                        nextAfter
                    )
                ) {
                    throw new Error(
                        'Reddit repeated a pagination cursor.'
                    );
                }

                seenCursors.add(
                    nextAfter
                );

                after =
                    nextAfter;

                /*
                 * Keep requests sequential and avoid hammering Reddit.
                 */
                await sleep(
                    100
                );
            }

            /*
             * Ensure the route did not change between the last request
             * finishing and committing the result.
             */
            if (!isSavedPage()) {
                return;
            }

            collectedPosts.clear();

            for (
                const [
                    url,
                    post
                ]
                of refreshed
            ) {
                collectedPosts.set(
                    url,
                    post
                );
            }

            saveStoredPosts();

            updateCounter();

            setStatus(
                `Loaded ${collectedPosts.size} saved post${collectedPosts.size === 1 ? '' : 's'} from Reddit.`
            );

            console.log(
                `[Reddit Saved Collector] Loaded ${collectedPosts.size} saved posts across ${page} listing page${page === 1 ? '' : 's'}.`
            );

        } catch (error) {
            if (
                error?.name ===
                'AbortError'
            ) {
                console.debug(
                    '[Reddit Saved Collector] Saved listing request cancelled.'
                );

                return;
            }

            console.error(
                '[Reddit Saved Collector]',
                error
            );

            setStatus(
                'Could not load Saved posts: ' +
                error.message
            );

        } finally {
            if (
                currentAbortController ===
                controller
            ) {
                currentAbortController =
                    null;
            }

            loading =
                false;

            updateButtons();
        }
    }

    // ============================================================
    // Export formatting
    // ============================================================

    function formatTitlesAndUrls(
        posts
    ) {
        return posts
            .map(
                post =>
                    `${post.title}\n${post.url}`
            )
            .join(
                '\n\n'
            );
    }

    function formatUrlsOnly(
        posts
    ) {
        return posts
            .map(
                post =>
                    post.url
            )
            .join(
                '\n'
            );
    }

    function formatTXT(
        posts
    ) {
        return posts
            .map(
                post => {
                    const lines = [
                        post.title,
                        post.url
                    ];

                    if (
                        post.subreddit
                    ) {
                        lines.push(
                            `Subreddit: r/${post.subreddit}`
                        );
                    }

                    if (
                        post.author
                    ) {
                        lines.push(
                            `Author: u/${post.author}`
                        );
                    }

                    if (
                        post.firstSeen
                    ) {
                        lines.push(
                            `Collected: ${post.firstSeen}`
                        );
                    }

                    return lines.join(
                        '\n'
                    );
                }
            )
            .join(
                '\n\n'
            );
    }

    function formatCSV(
        posts
    ) {
        const rows = [
            [
                'Title',
                'URL',
                'Subreddit',
                'Author',
                'Collected At'
            ]
        ];

        for (
            const post
            of posts
        ) {
            rows.push(
                [
                    post.title ||
                        '',

                    post.url ||
                        '',

                    post.subreddit ||
                        '',

                    post.author ||
                        '',

                    post.firstSeen ||
                        ''
                ]
            );
        }

        return rows
            .map(
                row =>
                    row
                        .map(
                            escapeCSV
                        )
                        .join(',')
            )
            .join(
                '\r\n'
            );
    }

    // ============================================================
    // Clipboard
    // ============================================================

    async function copyText(
        text,
        successMessage
    ) {
        try {
            await navigator
                .clipboard
                .writeText(
                    text
                );

            showToast(
                successMessage
            );

        } catch (error) {
            console.error(
                '[Reddit Saved Collector] Clipboard failed.',
                error
            );

            showToast(
                'Clipboard copy failed.'
            );
        }
    }

    // ============================================================
    // Downloads
    // ============================================================

    function downloadBlob(
        contents,
        filename,
        type
    ) {
        const blob =
            new Blob(
                [
                    contents
                ],
                {
                    type
                }
            );

        const url =
            URL.createObjectURL(
                blob
            );

        const anchor =
            document.createElement(
                'a'
            );

        anchor.href =
            url;

        anchor.download =
            filename;

        document.body
            ?.appendChild(
                anchor
            );

        anchor.click();

        anchor.remove();

        setTimeout(
            () =>
                URL.revokeObjectURL(
                    url
                ),
            1000
        );
    }

    function downloadCSV(
        posts
    ) {
        const csv =
            '\uFEFF' +
            formatCSV(
                posts
            );

        downloadBlob(
            csv,
            `reddit-saved-posts-${getDateString()}.csv`,
            'text/csv;charset=utf-8'
        );

        showToast(
            `CSV exported: ${posts.length} posts`
        );
    }

    function downloadTXT(
        posts
    ) {
        downloadBlob(
            formatTXT(
                posts
            ),
            `reddit-saved-posts-${getDateString()}.txt`,
            'text/plain;charset=utf-8'
        );

        showToast(
            `TXT exported: ${posts.length} posts`
        );
    }

    // ============================================================
    // Toast
    // ============================================================

    function createToast() {
        if (
            document.getElementById(
                'reddit-saved-collector-toast'
            )
        ) {
            return true;
        }

        if (
            !document.documentElement
        ) {
            return false;
        }

        const toast =
            document.createElement(
                'div'
            );

        toast.id =
            'reddit-saved-collector-toast';

        toast.style = `
            position:fixed;
            left:50%;
            bottom:20px;
            transform:translateX(-50%);
            z-index:2147483647;
            padding:8px 14px;
            background:#111;
            color:#fff;
            border:1px solid #00d8ff;
            border-radius:6px;
            font-family:system-ui,sans-serif;
            font-size:12px;
            pointer-events:none;
            opacity:0;
            transition:opacity .2s;
        `;

        document.documentElement
            .appendChild(
                toast
            );

        return true;
    }

    function showToast(
        message
    ) {
        if (!createToast()) {
            return;
        }

        const toast =
            document.getElementById(
                'reddit-saved-collector-toast'
            );

        if (!toast) {
            return;
        }

        toast.textContent =
            message;

        toast.style.opacity =
            '1';

        clearTimeout(
            toastTimer
        );

        toastTimer =
            setTimeout(
                () => {
                    toast.style.opacity =
                        '0';
                },
                1800
            );
    }

    // ============================================================
    // Floating panel
    // ============================================================

    function createPanel() {
        if (
            !collectorActive ||
            !isSavedPage()
        ) {
            return;
        }

        const existing =
            document.getElementById(
                'reddit-saved-collector-counter'
            );

        if (existing) {
            updateCounter();
            updateButtons();

            return;
        }

        if (
            !document.documentElement
        ) {
            return;
        }

        const box =
            document.createElement(
                'div'
            );

        box.id =
            'reddit-saved-collector-counter';

        box.style = `
            position:fixed;
            right:18px;
            bottom:18px;
            z-index:2147483645;
            width:210px;
            padding:10px 12px;
            background:#111;
            color:#fff;
            border:1px solid #00d8ff;
            border-radius:7px;
            box-shadow:0 4px 18px rgba(0,0,0,.5);
            font-family:system-ui,sans-serif;
            font-size:12px;
            text-align:center;
        `;

        box.innerHTML = `
            <div
                style="
                    color:#00d8ff;
                    font-size:10px;
                    margin-bottom:4px;
                "
            >
                REDDIT SAVED POSTS
            </div>

            <div
                id="reddit-saved-collector-count"
                style="
                    font-weight:700;
                    margin-bottom:5px;
                "
            >
                0 saved posts
            </div>

            <div
                id="reddit-saved-collector-status"
                style="
                    margin-bottom:7px;
                    color:#aaa;
                    font-size:10px;
                    line-height:1.3;
                "
            >
                Ready.
            </div>

            <button
                id="reddit-saved-refresh"
                type="button"
                style="
                    width:100%;
                    padding:6px;
                    background:#174f67;
                    color:#fff;
                    border:1px solid #277a9c;
                    border-radius:5px;
                    cursor:pointer;
                    font-weight:600;
                    font-size:11px;
                "
            >
                REFRESH SAVED POSTS
            </button>

            <button
                id="reddit-saved-links-only"
                type="button"
                style="
                    width:100%;
                    margin-top:5px;
                    padding:6px;
                    background:#222;
                    color:#fff;
                    border:1px solid #444;
                    border-radius:5px;
                    cursor:pointer;
                    font-weight:600;
                    font-size:11px;
                "
            >
                LINKS ONLY
            </button>

            <button
                id="reddit-saved-reset"
                type="button"
                style="
                    width:100%;
                    margin-top:5px;
                    padding:4px;
                    background:#311;
                    color:#bbb;
                    border:1px solid #522;
                    border-radius:5px;
                    cursor:pointer;
                    font-size:10px;
                "
            >
                RESET SESSION LIST
            </button>

            <div
                style="
                    margin-top:6px;
                    color:#666;
                    font-size:9px;
                "
            >
                v${VERSION} · Alt+R
            </div>
        `;

        document.documentElement
            .appendChild(
                box
            );

        box.querySelector(
            '#reddit-saved-refresh'
        )?.addEventListener(
            'click',
            loadSavedPosts
        );

        box.querySelector(
            '#reddit-saved-links-only'
        )?.addEventListener(
            'click',
            toggleLinksOnly
        );

        box.querySelector(
            '#reddit-saved-reset'
        )?.addEventListener(
            'click',
            resetCollection
        );

        updateCounter();
        updateButtons();
    }

    function removePanel() {
        document
            .getElementById(
                'reddit-saved-collector-counter'
            )
            ?.remove();
    }

    function updateCounter() {
        const element =
            document.getElementById(
                'reddit-saved-collector-count'
            );

        if (!element) {
            return;
        }

        element.textContent =
            `${collectedPosts.size} saved post${collectedPosts.size === 1 ? '' : 's'}`;
    }

    function setStatus(
        message
    ) {
        const element =
            document.getElementById(
                'reddit-saved-collector-status'
            );

        if (!element) {
            return;
        }

        element.textContent =
            message;
    }

    function updateButtons() {
        const refresh =
            document.getElementById(
                'reddit-saved-refresh'
            );

        const linksOnly =
            document.getElementById(
                'reddit-saved-links-only'
            );

        const reset =
            document.getElementById(
                'reddit-saved-reset'
            );

        for (
            const button
            of [
                refresh,
                linksOnly,
                reset
            ]
        ) {
            if (!button) {
                continue;
            }

            button.disabled =
                loading;

            button.style.opacity =
                loading
                    ? '0.55'
                    : '1';

            button.style.cursor =
                loading
                    ? 'default'
                    : 'pointer';
        }

        if (refresh) {
            refresh.textContent =
                loading
                    ? 'LOADING...'
                    : 'REFRESH SAVED POSTS';
        }
    }

    // ============================================================
    // Reset
    // ============================================================

    function resetCollection() {
        if (!isSavedPage()) {
            return;
        }

        if (
            !confirm(
                'Clear the locally collected Saved-post list for this browser session?\n\nThis does NOT unsave anything on Reddit.'
            )
        ) {
            return;
        }

        collectedPosts.clear();

        sessionStorage.removeItem(
            STORAGE_KEY
        );

        updateCounter();

        setStatus(
            'Session list cleared. Use Refresh Saved Posts to load it again.'
        );

        showToast(
            'Local session list cleared.'
        );
    }

    // ============================================================
    // Links-only overlay
    // ============================================================

    function renderLinksOnly() {
        if (
            linksOnlyActive ||
            !isSavedPage()
        ) {
            return;
        }

        const posts =
            [
                ...collectedPosts.values()
            ];

        if (!posts.length) {
            alert(
                'No saved Reddit posts have been loaded yet.'
            );

            return;
        }

        const overlay =
            document.createElement(
                'div'
            );

        overlay.id =
            'reddit-links-only-overlay';

        overlay.style = `
            position:fixed;
            inset:0;
            z-index:2147483646;
            overflow:auto;
            background:#0f0f0f;
            color:#eee;
            font-family:
                system-ui,
                -apple-system,
                BlinkMacSystemFont,
                "Segoe UI",
                sans-serif;
        `;

        const rows =
            posts
                .map(
                    (
                        post,
                        index
                    ) => {

                        const subreddit =
                            post.subreddit
                                ? `
                                    <span class="rlv-subreddit">
                                        r/${escapeHtml(post.subreddit)}
                                    </span>
                                `
                                : '';

                        const author =
                            post.author
                                ? `
                                    <span class="rlv-author">
                                        u/${escapeHtml(post.author)}
                                    </span>
                                `
                                : '';

                        return `
                            <div class="rlv-row">

                                <div class="rlv-number">
                                    ${index + 1}
                                </div>

                                <div class="rlv-content">

                                    <a
                                        class="rlv-title"
                                        href="${escapeHtml(post.url)}"
                                        target="_blank"
                                        rel="noopener noreferrer"
                                    >
                                        ${escapeHtml(post.title)}
                                    </a>

                                    <div class="rlv-meta">

                                        ${subreddit}
                                        ${author}

                                        <a
                                            class="rlv-url"
                                            href="${escapeHtml(post.url)}"
                                            target="_blank"
                                            rel="noopener noreferrer"
                                        >
                                            ${escapeHtml(post.url)}
                                        </a>

                                    </div>

                                </div>

                            </div>
                        `;
                    }
                )
                .join('');

        overlay.innerHTML = `
            <style>

                #reddit-links-only-overlay * {
                    box-sizing:border-box;
                }

                #rlv-root {
                    max-width:1200px;
                    margin:0 auto;
                    padding:28px;
                }

                .rlv-header {
                    position:sticky;
                    top:0;
                    z-index:10;
                    display:flex;
                    justify-content:space-between;
                    align-items:center;
                    gap:20px;
                    margin:-28px -28px 20px -28px;
                    padding:16px 28px;
                    background:#111;
                    border-bottom:1px solid #333;
                }

                .rlv-heading {
                    color:#fff;
                    font-size:20px;
                    font-weight:700;
                }

                .rlv-count {
                    margin-top:3px;
                    color:#999;
                    font-size:12px;
                }

                .rlv-buttons {
                    display:flex;
                    gap:8px;
                    flex-wrap:wrap;
                    justify-content:flex-end;
                }

                .rlv-button {
                    padding:8px 12px;
                    border:1px solid #444;
                    border-radius:6px;
                    background:#222;
                    color:#fff;
                    cursor:pointer;
                    font-size:12px;
                    font-weight:600;
                }

                .rlv-button:hover {
                    background:#333;
                }

                .rlv-row {
                    display:grid;
                    grid-template-columns:42px 1fr;
                    gap:10px;
                    padding:13px 8px;
                    border-bottom:1px solid #292929;
                }

                .rlv-row:hover {
                    background:#161616;
                }

                .rlv-number {
                    padding-top:2px;
                    color:#666;
                    text-align:right;
                    font-size:12px;
                }

                .rlv-content {
                    min-width:0;
                }

                .rlv-title {
                    display:inline-block;
                    color:#e8e8e8 !important;
                    font-size:15px;
                    font-weight:600;
                    line-height:1.35;
                    text-decoration:none !important;
                }

                .rlv-title:hover {
                    color:#00d8ff !important;
                    text-decoration:underline !important;
                }

                .rlv-meta {
                    display:flex;
                    align-items:center;
                    gap:8px;
                    min-width:0;
                    margin-top:5px;
                    font-size:11px;
                }

                .rlv-subreddit,
                .rlv-author {
                    flex:0 0 auto;
                    padding:2px 5px;
                    border-radius:3px;
                    background:#242424;
                    color:#aaa;
                }

                .rlv-url {
                    display:block;
                    min-width:0;
                    overflow:hidden;
                    text-overflow:ellipsis;
                    white-space:nowrap;
                    color:#666 !important;
                    text-decoration:none !important;
                }

                .rlv-url:hover {
                    color:#aaa !important;
                    text-decoration:underline !important;
                }

                @media (max-width:800px) {

                    #rlv-root {
                        padding:16px;
                    }

                    .rlv-header {
                        align-items:flex-start;
                        flex-direction:column;
                        margin:-16px -16px 16px -16px;
                        padding:12px 16px;
                    }

                    .rlv-buttons {
                        justify-content:flex-start;
                    }

                    .rlv-row {
                        grid-template-columns:30px 1fr;
                    }
                }

            </style>

            <div id="rlv-root">

                <div class="rlv-header">

                    <div>

                        <div class="rlv-heading">
                            Reddit Saved — Saved Posts
                        </div>

                        <div class="rlv-count">
                            ${posts.length}
                            saved post${posts.length === 1 ? '' : 's'}
                            returned by Reddit
                        </div>

                    </div>

                    <div class="rlv-buttons">

                        <button
                            id="rlv-copy-title-url"
                            class="rlv-button"
                            type="button"
                        >
                            COPY TITLES + URLS
                        </button>

                        <button
                            id="rlv-copy-urls"
                            class="rlv-button"
                            type="button"
                        >
                            COPY URLS
                        </button>

                        <button
                            id="rlv-download-csv"
                            class="rlv-button"
                            type="button"
                        >
                            DOWNLOAD CSV
                        </button>

                        <button
                            id="rlv-download-txt"
                            class="rlv-button"
                            type="button"
                        >
                            DOWNLOAD TXT
                        </button>

                        <button
                            id="rlv-close"
                            class="rlv-button"
                            type="button"
                        >
                            RETURN TO REDDIT
                        </button>

                    </div>

                </div>

                <div id="rlv-list">
                    ${rows}
                </div>

            </div>
        `;

        document.documentElement
            .appendChild(
                overlay
            );

        linksOnlyActive =
            true;

        overlay
            .querySelector(
                '#rlv-close'
            )
            ?.addEventListener(
                'click',
                closeLinksOnly
            );

        overlay
            .querySelector(
                '#rlv-copy-title-url'
            )
            ?.addEventListener(
                'click',
                () =>
                    copyText(
                        formatTitlesAndUrls(
                            posts
                        ),
                        `Copied ${posts.length} titles + URLs`
                    )
            );

        overlay
            .querySelector(
                '#rlv-copy-urls'
            )
            ?.addEventListener(
                'click',
                () =>
                    copyText(
                        formatUrlsOnly(
                            posts
                        ),
                        `Copied ${posts.length} URLs`
                    )
            );

        overlay
            .querySelector(
                '#rlv-download-csv'
            )
            ?.addEventListener(
                'click',
                () =>
                    downloadCSV(
                        posts
                    )
            );

        overlay
            .querySelector(
                '#rlv-download-txt'
            )
            ?.addEventListener(
                'click',
                () =>
                    downloadTXT(
                        posts
                    )
            );
    }

    function closeLinksOnly() {
        document
            .getElementById(
                'reddit-links-only-overlay'
            )
            ?.remove();

        linksOnlyActive =
            false;
    }

    function toggleLinksOnly() {
        if (
            linksOnlyActive
        ) {
            closeLinksOnly();
            return;
        }

        if (
            isSavedPage()
        ) {
            renderLinksOnly();
        }
    }

    // ============================================================
    // Collector lifecycle
    // ============================================================

    function startCollector() {
        if (!isSavedPage()) {
            stopCollector();
            return;
        }

        if (
            collectorActive
        ) {
            createPanel();
            return;
        }

        collectorActive =
            true;

        createPanel();

        updateCounter();

        /*
         * Automatically refresh when entering Saved.
         *
         * Existing session data may appear immediately while Reddit's
         * current Saved listing is fetched.
         */
        loadSavedPosts();

        console.log(
            '[Reddit Saved Collector] Saved API collector activated.'
        );
    }

    function stopCollector() {
        if (
            currentAbortController
        ) {
            currentAbortController.abort();

            currentAbortController =
                null;
        }

        loading =
            false;

        collectorActive =
            false;

        removePanel();

        if (
            linksOnlyActive
        ) {
            closeLinksOnly();
        }
    }

    function syncCollectorToRoute() {
        lastKnownUrl =
            location.href;

        if (
            isSavedPage()
        ) {
            startCollector();

        } else {
            stopCollector();
        }
    }

    function scheduleRouteSync(
        delay = 0
    ) {
        clearTimeout(
            routeSyncTimer
        );

        routeSyncTimer =
            setTimeout(
                syncCollectorToRoute,
                delay
            );
    }

    // ============================================================
    // Reddit SPA navigation
    // ============================================================

    function hookHistoryMethod(
        methodName
    ) {
        const original =
            history[
                methodName
            ];

        if (
            typeof original !==
            'function'
        ) {
            return;
        }

        history[
            methodName
        ] =
            function (...args) {
                const result =
                    original.apply(
                        this,
                        args
                    );

                scheduleRouteSync(
                    0
                );

                return result;
            };
    }

    hookHistoryMethod(
        'pushState'
    );

    hookHistoryMethod(
        'replaceState'
    );

    window.addEventListener(
        'popstate',
        () =>
            scheduleRouteSync(
                0
            )
    );

    window.addEventListener(
        'hashchange',
        () =>
            scheduleRouteSync(
                0
            )
    );

    window.addEventListener(
        'pageshow',
        () =>
            scheduleRouteSync(
                0
            )
    );

    document.addEventListener(
        'visibilitychange',
        () => {
            if (
                document.visibilityState ===
                'visible'
            ) {
                scheduleRouteSync(
                    0
                );
            }
        }
    );

    /*
     * Lightweight route fallback.
     *
     * No DOM content is scanned here.
     */
    setInterval(
        () => {
            if (
                location.href !==
                lastKnownUrl
            ) {
                console.debug(
                    '[Reddit Saved Collector] Route changed:',
                    lastKnownUrl,
                    '→',
                    location.href
                );

                syncCollectorToRoute();
            }
        },
        ROUTE_POLL_MS
    );

    // ============================================================
    // Alt + R
    // ============================================================

    document.addEventListener(
        'keydown',
        event => {

            const target =
                event.target;

            const typing =
                target instanceof
                    HTMLInputElement ||
                target instanceof
                    HTMLTextAreaElement ||
                target?.isContentEditable;

            if (typing) {
                return;
            }

            if (
                event.altKey &&
                !event.ctrlKey &&
                !event.shiftKey &&
                event.code ===
                    'KeyR'
            ) {
                if (
                    !isSavedPage()
                ) {
                    return;
                }

                event.preventDefault();

                toggleLinksOnly();
            }
        }
    );

    // ============================================================
    // Start
    // ============================================================

    loadStoredPosts();

    syncCollectorToRoute();

    console.log(
        `[Reddit Saved Collector] v${VERSION} API collector installed.`
    );

})();