DatafetchPro
    May 25, 20265 min read0 views

    How to Export TikTok Comments Easily?

    Need TikTok comments fast? Export comments to CSV, JSON, or Markdown using this simple web scraping and data extraction script.

    Export TikTok Comments Without Manual Copy-Paste

    TikTok comments contain valuable audience insights, customer opinions, and trending discussions, but collecting them manually can quickly become frustrating. This TikTok Comment Exporter script simplifies the entire process by allowing you to export comments directly into CSV, JSON, Markdown, or plain text formats.




    Built for Data Extraction & Web Scraping

    The script is designed for fast and efficient data extraction workflows. With a floating control panel, live comment counter, quick scanning system, and instant export options, it becomes much easier to organize TikTok comment data for research, reporting, or automation tasks.

    Whether you're working on web scraping projects, audience analysis, sentiment tracking, or social media monitoring, the exporter helps reduce repetitive manual work.

    Simple Interface With Powerful Features

    The clean UI keeps everything accessible without interrupting browsing. You can preview comments before exporting, copy all comments instantly, and download structured datasets in multiple formats with a single click.

    The script includes:

    • CSV export for spreadsheets
    • JSON export for developers and APIs
    • Markdown export for reports
    • Clipboard copy support
    • Live comment statistics
    • Lightweight floating interface

    Useful for Automation & Research

    This tool is especially useful for marketers, creators, researchers, and developers building web automation systems around social media platforms. Instead of manually collecting information, you can quickly extract organized TikTok comment data and use it in your own workflows or analytics systems.

    If you regularly work with social media scraping, automation scripts, or online data collection, this exporter can save significant time while keeping your process clean and efficient.



    // ==UserScript==

    // @name TikTok Comment Exporter ✦ Pro

    // @namespace https://github.com/comment-exporter

    // @version 2.0.0

    // @description Export TikTok comments with full stats — CSV, JSON, Markdown, or clipboard. Floating panel with live counter, filters, and beautiful UI.

    // @author ali

    // @match https://www.tiktok.com/*

    // @grant GM_setClipboard

    // @grant GM_download

    // @run-at document-idle

    // ==/UserScript==




    (function () {

    'use strict';



    /* ─────────────────────────────────────────────

    CONSTANTS & STATE

    ───────────────────────────────────────────── */

    const SELECTORS = {

    commentWrapper: '.e1bjtz6z10',

    usernameLink: 'a[href^="/@"]',

    commentText: '[data-e2e="comment-level-1"] span',

    likeContainer: '.e16vxroe0',

    timestamp: '.e1bjtz6z6 > span',

    totalCount: '.ef1nmik2 span:last-child',

    };



    let panel = null;

    let isOpen = false;

    let lastCommentCount = 0;

    let scanTimer = null;



    /* ─────────────────────────────────────────────

    INJECT STYLES

    ───────────────────────────────────────────── */

    const injectStyles = () => {

    const style = document.createElement('style');

    style.textContent = `

    @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&display=swap');



    :root {

    --ce-bg: #0a0a0f;

    --ce-surface: #111118;

    --ce-border: rgba(255,255,255,0.07);

    --ce-accent: #7c6af7;

    --ce-accent2: #f76a8a;

    --ce-text: #e8e8f0;

    --ce-muted: #6b6b80;

    --ce-success: #4ade80;

    --ce-warning: #fbbf24;

    --ce-radius: 16px;

    --ce-glow: 0 0 40px rgba(124,106,247,0.15);

    }



    #ce-fab {

    position: fixed;

    bottom: 88px;

    right: 18px;

    z-index: 999999;

    width: 52px;

    height: 52px;

    border-radius: 50%;

    background: linear-gradient(135deg, var(--ce-accent), var(--ce-accent2));

    border: none;

    cursor: pointer;

    display: flex;

    align-items: center;

    justify-content: center;

    box-shadow: 0 4px 24px rgba(124,106,247,0.45), 0 1px 4px rgba(0,0,0,0.5);

    transition: transform 0.2s cubic-bezier(.34,1.56,.64,1), box-shadow 0.2s;

    font-family: 'Syne', sans-serif;

    }

    #ce-fab:hover {

    transform: scale(1.12) rotate(-5deg);

    box-shadow: 0 6px 32px rgba(124,106,247,0.65);

    }

    #ce-fab:active { transform: scale(0.96); }

    #ce-fab svg { pointer-events: none; }



    #ce-badge {

    position: absolute;

    top: -4px;

    right: -4px;

    background: var(--ce-accent2);

    color: #fff;

    font-family: 'DM Mono', monospace;

    font-size: 9px;

    font-weight: 500;

    padding: 2px 5px;

    border-radius: 20px;

    min-width: 18px;

    text-align: center;

    line-height: 14px;

    border: 2px solid var(--ce-bg);

    display: none;

    animation: ce-pop 0.3s cubic-bezier(.34,1.56,.64,1);

    }



    @keyframes ce-pop {

    from { transform: scale(0); }

    to { transform: scale(1); }

    }



    #ce-panel {

    position: fixed;

    bottom: 150px;

    right: 18px;

    z-index: 999998;

    width: 360px;

    background: var(--ce-bg);

    border: 1px solid var(--ce-border);

    border-radius: var(--ce-radius);

    box-shadow: var(--ce-glow), 0 32px 80px rgba(0,0,0,0.7);

    font-family: 'Syne', sans-serif;

    color: var(--ce-text);

    overflow: hidden;

    transform-origin: bottom right;

    animation: ce-panel-in 0.28s cubic-bezier(.34,1.36,.64,1);

    backdrop-filter: blur(12px);

    }



    @keyframes ce-panel-in {

    from { transform: scale(0.7) translateY(20px); opacity: 0; }

    to { transform: scale(1) translateY(0); opacity: 1; }

    }



    #ce-panel-header {

    padding: 16px 18px 14px;

    display: flex;

    align-items: center;

    gap: 10px;

    border-bottom: 1px solid var(--ce-border);

    background: linear-gradient(135deg, rgba(124,106,247,0.08), rgba(247,106,138,0.05));

    }



    #ce-panel-header h2 {

    margin: 0;

    font-size: 15px;

    font-weight: 800;

    letter-spacing: -0.3px;

    flex: 1;

    background: linear-gradient(90deg, var(--ce-accent), var(--ce-accent2));

    -webkit-background-clip: text;

    -webkit-text-fill-color: transparent;

    background-clip: text;

    }



    #ce-close-btn {

    background: none;

    border: none;

    cursor: pointer;

    color: var(--ce-muted);

    padding: 4px;

    border-radius: 8px;

    transition: color 0.15s, background 0.15s;

    line-height: 1;

    }

    #ce-close-btn:hover { color: var(--ce-text); background: rgba(255,255,255,0.06); }



    #ce-stats-row {

    display: grid;

    grid-template-columns: repeat(3, 1fr);

    gap: 1px;

    background: var(--ce-border);

    border-bottom: 1px solid var(--ce-border);

    }



    .ce-stat {

    background: var(--ce-surface);

    padding: 12px 14px 10px;

    text-align: center;

    }

    .ce-stat-val {

    display: block;

    font-size: 20px;

    font-weight: 800;

    font-family: 'DM Mono', monospace;

    color: var(--ce-text);

    line-height: 1;

    margin-bottom: 3px;

    }

    .ce-stat-lbl {

    display: block;

    font-size: 9px;

    font-weight: 600;

    text-transform: uppercase;

    letter-spacing: 0.8px;

    color: var(--ce-muted);

    }



    #ce-panel-body {

    padding: 14px;

    }



    /* Scan button */

    #ce-scan-btn {

    width: 100%;

    padding: 11px;

    border-radius: 10px;

    background: linear-gradient(135deg, var(--ce-accent), #5b4ee0);

    border: none;

    color: #fff;

    font-family: 'Syne', sans-serif;

    font-size: 13px;

    font-weight: 700;

    cursor: pointer;

    display: flex;

    align-items: center;

    justify-content: center;

    gap: 7px;

    transition: opacity 0.15s, transform 0.15s;

    margin-bottom: 12px;

    position: relative;

    overflow: hidden;

    }

    #ce-scan-btn::before {

    content: '';

    position: absolute;

    inset: 0;

    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent);

    transform: translateX(-100%);

    transition: transform 0.5s;

    }

    #ce-scan-btn:hover::before { transform: translateX(100%); }

    #ce-scan-btn:hover { opacity: 0.9; transform: translateY(-1px); }

    #ce-scan-btn:active { transform: scale(0.98); }

    #ce-scan-btn.scanning { background: linear-gradient(135deg, #3d3d5c, #2d2d45); cursor: default; }



    /* Export buttons grid */

    #ce-export-grid {

    display: grid;

    grid-template-columns: 1fr 1fr;

    gap: 8px;

    margin-bottom: 12px;

    }



    .ce-export-btn {

    padding: 10px 8px;

    border-radius: 10px;

    border: 1px solid var(--ce-border);

    background: var(--ce-surface);

    color: var(--ce-text);

    font-family: 'Syne', sans-serif;

    font-size: 12px;

    font-weight: 700;

    cursor: pointer;

    display: flex;

    flex-direction: column;

    align-items: center;

    gap: 5px;

    transition: border-color 0.15s, background 0.15s, transform 0.15s;

    position: relative;

    overflow: hidden;

    }

    .ce-export-btn:hover {

    border-color: rgba(124,106,247,0.4);

    background: rgba(124,106,247,0.08);

    transform: translateY(-2px);

    }

    .ce-export-btn:active { transform: scale(0.97); }

    .ce-export-btn .ce-btn-icon { font-size: 18px; line-height: 1; }

    .ce-export-btn .ce-btn-format {

    font-size: 10px;

    font-weight: 600;

    text-transform: uppercase;

    letter-spacing: 0.5px;

    color: var(--ce-muted);

    }



    /* Flash success on buttons */

    .ce-export-btn.ce-flash {

    background: rgba(74,222,128,0.12) !important;

    border-color: var(--ce-success) !important;

    color: var(--ce-success) !important;

    }



    /* Progress bar */

    #ce-progress-wrap {

    height: 3px;

    background: rgba(255,255,255,0.05);

    border-radius: 99px;

    overflow: hidden;

    margin-bottom: 10px;

    display: none;

    }

    #ce-progress-bar {

    height: 100%;

    background: linear-gradient(90deg, var(--ce-accent), var(--ce-accent2));

    border-radius: 99px;

    width: 0%;

    transition: width 0.4s ease;

    }



    /* Status message */

    #ce-status {

    font-size: 11px;

    color: var(--ce-muted);

    text-align: center;

    min-height: 16px;

    font-family: 'DM Mono', monospace;

    letter-spacing: 0.2px;

    }

    #ce-status.ok { color: var(--ce-success); }

    #ce-status.warn { color: var(--ce-warning); }



    /* Preview list */

    #ce-preview {

    margin-top: 12px;

    border-top: 1px solid var(--ce-border);

    padding-top: 10px;

    max-height: 160px;

    overflow-y: auto;

    scrollbar-width: thin;

    scrollbar-color: rgba(124,106,247,0.3) transparent;

    }

    #ce-preview::-webkit-scrollbar { width: 4px; }

    #ce-preview::-webkit-scrollbar-thumb { background: rgba(124,106,247,0.3); border-radius: 4px; }



    .ce-preview-item {

    padding: 7px 0;

    border-bottom: 1px solid rgba(255,255,255,0.04);

    display: flex;

    gap: 8px;

    animation: ce-fade-in 0.2s ease;

    }

    @keyframes ce-fade-in {

    from { opacity: 0; transform: translateY(4px); }

    to { opacity: 1; transform: translateY(0); }

    }

    .ce-preview-item:last-child { border-bottom: none; }

    .ce-preview-user {

    font-size: 11px;

    font-weight: 700;

    color: var(--ce-accent);

    white-space: nowrap;

    overflow: hidden;

    text-overflow: ellipsis;

    min-width: 70px;

    max-width: 90px;

    }

    .ce-preview-text {

    font-size: 11px;

    color: var(--ce-muted);

    overflow: hidden;

    text-overflow: ellipsis;

    white-space: nowrap;

    flex: 1;

    }

    .ce-preview-likes {

    font-family: 'DM Mono', monospace;

    font-size: 10px;

    color: var(--ce-accent2);

    white-space: nowrap;

    align-self: center;

    }



    #ce-preview-empty {

    font-size: 11px;

    color: var(--ce-muted);

    text-align: center;

    padding: 12px 0;

    font-family: 'DM Mono', monospace;

    }



    /* Copy all btn */

    #ce-copy-all-btn {

    width: 100%;

    margin-top: 8px;

    padding: 9px;

    border-radius: 10px;

    border: 1px solid var(--ce-border);

    background: rgba(255,255,255,0.03);

    color: var(--ce-muted);

    font-family: 'Syne', sans-serif;

    font-size: 11px;

    font-weight: 600;

    cursor: pointer;

    display: flex;

    align-items: center;

    justify-content: center;

    gap: 6px;

    transition: all 0.15s;

    text-transform: uppercase;

    letter-spacing: 0.5px;

    }

    #ce-copy-all-btn:hover {

    border-color: rgba(255,255,255,0.15);

    background: rgba(255,255,255,0.06);

    color: var(--ce-text);

    }

    `;

    document.head.appendChild(style);

    };



    /* ─────────────────────────────────────────────

    SCRAPE COMMENTS

    ───────────────────────────────────────────── */

    function scrapeComments() {

    const wrappers = document.querySelectorAll(SELECTORS.commentWrapper);

    const comments = [];



    wrappers.forEach((wrap, idx) => {

    try {

    // Username display name

    const usernameEl = wrap.querySelector('[data-e2e="comment-username-1"] a p');

    const username = usernameEl ? usernameEl.textContent.trim() : 'Unknown';



    // @handle from href

    const usernameLink = wrap.querySelector('[data-e2e="comment-username-1"] a');

    const handle = usernameLink ? usernameLink.getAttribute('href').replace('/@', '@') : '';



    // Comment text

    const textEl = wrap.querySelector('[data-e2e="comment-level-1"] span');

    const text = textEl ? textEl.textContent.trim() : '';



    // Likes (from aria-label "X likes")

    const likeEl = wrap.querySelector('[aria-label*="likes"]');

    let likes = 0;

    if (likeEl) {

    const match = likeEl.getAttribute('aria-label').match(/(\d[\d,]*)\s+like/i);

    if (match) likes = parseInt(match[1].replace(/,/g, ''), 10);

    }



    // Timestamp

    const tsEl = wrap.querySelector('.e1bjtz6z6 > span');

    const timestamp = tsEl ? tsEl.textContent.trim() : '';



    // Avatar URL

    const avatarEl = wrap.querySelector('img.e1iqrkv71');

    const avatarUrl = avatarEl ? avatarEl.src : '';



    if (text) {

    comments.push({ index: idx + 1, username, handle, text, likes, timestamp, avatarUrl });

    }

    } catch (e) { /* skip malformed nodes */ }

    });



    return comments;

    }



    /* ─────────────────────────────────────────────

    EXPORT FUNCTIONS

    ───────────────────────────────────────────── */

    function toCSV(comments) {

    const header = ['#', 'Username', 'Handle', 'Comment', 'Likes', 'Timestamp'];

    const escape = v => `"${String(v).replace(/"/g, '""')}"`;

    const rows = comments.map(c => [c.index, escape(c.username), escape(c.handle), escape(c.text), c.likes, escape(c.timestamp)].join(','));

    return [header.join(','), ...rows].join('\r\n');

    }



    function toJSON(comments) {

    return JSON.stringify({

    exported_at: new Date().toISOString(),

    page_url: location.href,

    total_scraped: comments.length,

    total_likes: comments.reduce((s, c) => s + c.likes, 0),

    comments,

    }, null, 2);

    }



    function toMarkdown(comments) {

    const header = `# TikTok Comments Export\n**URL:** ${location.href}\n**Exported:** ${new Date().toLocaleString()}\n**Comments scraped:** ${comments.length}\n\n---\n\n`;

    const rows = comments.map(c =>

    `### ${c.index}. ${c.username} (${c.handle})\n> ${c.text}\n\n❤️ **${c.likes} likes** · 🕐 ${c.timestamp}\n`

    ).join('\n---\n\n');

    return header + rows;

    }



    function toPlainText(comments) {

    return comments.map(c =>

    `[${c.index}] ${c.username} (${c.handle}) — ${c.likes} likes — ${c.timestamp}\n${c.text}`

    ).join('\n\n');

    }



    /* ─────────────────────────────────────────────

    DOWNLOAD HELPER

    ───────────────────────────────────────────── */

    function downloadFile(content, filename, mime) {

    const blob = new Blob([content], { type: mime });

    const url = URL.createObjectURL(blob);

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

    a.href = url; a.download = filename;

    document.body.appendChild(a); a.click();

    document.body.removeChild(a);

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

    }



    function getFilename(ext) {

    const slug = location.pathname.replace(/\//g, '_').slice(0, 40) || 'tiktok';

    const ts = new Date().toISOString().slice(0, 16).replace(/[T:]/g, '-');

    return `comments_${slug}_${ts}.${ext}`;

    }



    /* ─────────────────────────────────────────────

    UI BUILDER

    ───────────────────────────────────────────── */

    function buildPanel() {

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

    el.id = 'ce-panel';

    el.innerHTML = `

    <div id="ce-panel-header">

    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="url(#cegrad)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">

    <defs><linearGradient id="cegrad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#7c6af7"/><stop offset="100%" stop-color="#f76a8a"/></linearGradient></defs>

    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>

    </svg>

    <h2>Comment Exporter Pro</h2>

    <button id="ce-close-btn" title="Close">

    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>

    </button>

    </div>



    <div id="ce-stats-row">

    <div class="ce-stat">

    <span class="ce-stat-val" id="ce-stat-scraped">0</span>

    <span class="ce-stat-lbl">Scraped</span>

    </div>

    <div class="ce-stat">

    <span class="ce-stat-val" id="ce-stat-likes">0</span>

    <span class="ce-stat-lbl">Total Likes</span>

    </div>

    <div class="ce-stat">

    <span class="ce-stat-val" id="ce-stat-total">—</span>

    <span class="ce-stat-lbl">Post Total</span>

    </div>

    </div>



    <div id="ce-panel-body">

    <button id="ce-scan-btn">

    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>

    Scan Comments

    </button>



    <div id="ce-progress-wrap"><div id="ce-progress-bar"></div></div>

    <div id="ce-status">Click Scan to scrape visible comments</div>



    <div id="ce-export-grid">

    <button class="ce-export-btn" id="ce-btn-csv">

    <span class="ce-btn-icon">📊</span>

    <span>Download</span>

    <span class="ce-btn-format">CSV</span>

    </button>

    <button class="ce-export-btn" id="ce-btn-json">

    <span class="ce-btn-icon">🗂️</span>

    <span>Download</span>

    <span class="ce-btn-format">JSON</span>

    </button>

    <button class="ce-export-btn" id="ce-btn-md">

    <span class="ce-btn-icon">📝</span>

    <span>Download</span>

    <span class="ce-btn-format">Markdown</span>

    </button>

    <button class="ce-export-btn" id="ce-btn-copy">

    <span class="ce-btn-icon">📋</span>

    <span>Copy JSON</span>

    <span class="ce-btn-format">Clipboard</span>

    </button>

    </div>



    <button id="ce-copy-all-btn">

    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>

    Copy All as Plain Text

    </button>



    <div id="ce-preview">

    <div id="ce-preview-empty">↑ Scan first to preview comments</div>

    </div>

    </div>

    `;

    return el;

    }



    /* ─────────────────────────────────────────────

    PANEL LOGIC

    ───────────────────────────────────────────── */

    let cachedComments = [];



    function setStatus(msg, type = '') {

    const el = document.getElementById('ce-status');

    if (!el) return;

    el.textContent = msg;

    el.className = type;

    }



    function updateStats(comments) {

    const totalLikes = comments.reduce((s, c) => s + c.likes, 0);

    const fmtNum = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);



    document.getElementById('ce-stat-scraped').textContent = comments.length;

    document.getElementById('ce-stat-likes').textContent = fmtNum(totalLikes);



    // Try to read post's own displayed total

    const totalEl = document.querySelector(SELECTORS.totalCount);

    document.getElementById('ce-stat-total').textContent = totalEl ? totalEl.textContent.trim() : '—';



    // Update FAB badge

    const badge = document.getElementById('ce-badge');

    if (badge && comments.length > 0) {

    badge.textContent = comments.length;

    badge.style.display = 'block';

    }

    }



    function renderPreview(comments) {

    const preview = document.getElementById('ce-preview');

    if (!preview) return;



    if (!comments.length) {

    preview.innerHTML = '<div id="ce-preview-empty">No comments found in view</div>';

    return;

    }



    preview.innerHTML = comments.slice(0, 30).map(c => `

    <div class="ce-preview-item">

    <span class="ce-preview-user">${escapeHtml(c.username)}</span>

    <span class="ce-preview-text">${escapeHtml(c.text)}</span>

    <span class="ce-preview-likes">♥ ${c.likes}</span>

    </div>

    `).join('');

    }



    function escapeHtml(str) {

    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

    }



    function flashBtn(btn) {

    btn.classList.add('ce-flash');

    setTimeout(() => btn.classList.remove('ce-flash'), 1200);

    }



    function doScan() {

    const btn = document.getElementById('ce-scan-btn');

    const pg = document.getElementById('ce-progress-wrap');

    const bar = document.getElementById('ce-progress-bar');

    if (!btn) return;



    btn.classList.add('scanning');

    btn.textContent = '⟳ Scanning…';

    pg.style.display = 'block';

    bar.style.width = '0%';

    setStatus('Reading DOM…');



    // Animate progress

    let p = 0;

    const t = setInterval(() => {

    p = Math.min(p + Math.random() * 18, 88);

    bar.style.width = p + '%';

    }, 80);



    setTimeout(() => {

    clearInterval(t);

    cachedComments = scrapeComments();

    bar.style.width = '100%';



    setTimeout(() => {

    pg.style.display = 'none';

    bar.style.width = '0%';

    btn.classList.remove('scanning');

    btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="1 4 1 10 7 10"/><polyline points="23 20 23 14 17 14"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg> Scan Again`;



    updateStats(cachedComments);

    renderPreview(cachedComments);

    setStatus(cachedComments.length ? `${cachedComments.length} comments ready to export` : '⚠ No comments found — scroll & retry', cachedComments.length ? 'ok' : 'warn');

    }, 300);

    }, 600);

    }



    function bindEvents() {

    document.getElementById('ce-close-btn').addEventListener('click', () => {

    panel.remove(); panel = null; isOpen = false;

    });



    document.getElementById('ce-scan-btn').addEventListener('click', doScan);



    document.getElementById('ce-btn-csv').addEventListener('click', function() {

    if (!cachedComments.length) { setStatus('⚠ Scan first!', 'warn'); return; }

    downloadFile(toCSV(cachedComments), getFilename('csv'), 'text/csv;charset=utf-8;');

    flashBtn(this);

    setStatus(`✓ CSV downloaded (${cachedComments.length} rows)`, 'ok');

    });



    document.getElementById('ce-btn-json').addEventListener('click', function() {

    if (!cachedComments.length) { setStatus('⚠ Scan first!', 'warn'); return; }

    downloadFile(toJSON(cachedComments), getFilename('json'), 'application/json');

    flashBtn(this);

    setStatus(`✓ JSON downloaded`, 'ok');

    });



    document.getElementById('ce-btn-md').addEventListener('click', function() {

    if (!cachedComments.length) { setStatus('⚠ Scan first!', 'warn'); return; }

    downloadFile(toMarkdown(cachedComments), getFilename('md'), 'text/markdown');

    flashBtn(this);

    setStatus(`✓ Markdown downloaded`, 'ok');

    });



    document.getElementById('ce-btn-copy').addEventListener('click', function() {

    if (!cachedComments.length) { setStatus('⚠ Scan first!', 'warn'); return; }

    try {

    if (typeof GM_setClipboard !== 'undefined') {

    GM_setClipboard(toJSON(cachedComments), 'text');

    } else {

    navigator.clipboard.writeText(toJSON(cachedComments));

    }

    flashBtn(this);

    setStatus(`✓ JSON copied to clipboard!`, 'ok');

    } catch(e) { setStatus('✗ Clipboard access denied', 'warn'); }

    });



    document.getElementById('ce-copy-all-btn').addEventListener('click', function() {

    if (!cachedComments.length) { setStatus('⚠ Scan first!', 'warn'); return; }

    try {

    const text = toPlainText(cachedComments);

    if (typeof GM_setClipboard !== 'undefined') {

    GM_setClipboard(text, 'text');

    } else {

    navigator.clipboard.writeText(text);

    }

    setStatus(`${cachedComments.length} comments copied as text!`, 'ok');

    } catch(e) { setStatus('✗ Clipboard access denied', 'warn'); }

    });

    }



    /* ─────────────────────────────────────────────

    FAB

    ───────────────────────────────────────────── */

    function buildFAB() {

    const fab = document.createElement('button');

    fab.id = 'ce-fab';

    fab.title = 'Comment Exporter Pro';

    fab.innerHTML = `

    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">

    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>

    <line x1="9" y1="10" x2="15" y2="10"/>

    <line x1="12" y1="7" x2="12" y2="13"/>

    </svg>

    <span id="ce-badge">0</span>

    `;

    fab.addEventListener('click', () => {

    if (isOpen && panel) { panel.remove(); panel = null; isOpen = false; return; }

    panel = buildPanel();

    document.body.appendChild(panel);

    bindEvents();

    isOpen = true;

    });

    return fab;

    }



    /* ─────────────────────────────────────────────

    LIVE COMMENT WATCHER

    ───────────────────────────────────────────── */

    function watchComments() {

    const observer = new MutationObserver(() => {

    const count = document.querySelectorAll(SELECTORS.commentWrapper).length;

    if (count !== lastCommentCount && count > 0) {

    lastCommentCount = count;

    // Update badge passively if panel is open

    if (isOpen && panel) {

    const badge = document.getElementById('ce-badge');

    if (badge) { badge.textContent = count; badge.style.display = 'block'; }

    }

    }

    });

    observer.observe(document.body, { childList: true, subtree: true });

    }



    /* ─────────────────────────────────────────────

    INIT

    ───────────────────────────────────────────── */

    function init() {

    injectStyles();

    document.body.appendChild(buildFAB());

    watchComments();

    }



    if (document.readyState === 'loading') {

    document.addEventListener('DOMContentLoaded', init);

    } else {

    init();

    }



    // Re-init FAB on TikTok SPA navigation

    let lastHref = location.href;

    setInterval(() => {

    if (location.href !== lastHref) {

    lastHref = location.href;

    if (!document.getElementById('ce-fab')) {

    document.body.appendChild(buildFAB());

    }

    }

    }, 1200);



    })();
    0