TurboScribe Quick Copy

Adds a draggable floating button to copy the transcript text to your clipboard with one click. Strips DOM clutter, optionally removes timestamps, auto-detects browser language.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         TurboScribe Quick Copy
// @namespace    https://greasyfork.org/users/1602450-gonzalo-uma%C3%B1a
// @version      1.2.0
// @description  Adds a draggable floating button to copy the transcript text to your clipboard with one click. Strips DOM clutter, optionally removes timestamps, auto-detects browser language.
// @author       Gonzalo Umaña
// @match        *://turboscribe.ai/*
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== USER CONFIGURATION =====
    // Change to `true` to include timestamps like (0:02) in the copied text.
    // This is the default for users who haven't toggled the setting via the
    // Tampermonkey menu. Once you toggle the menu option, that choice is saved
    // and takes priority over this constant.
    const INCLUDE_TIMESTAMPS_DEFAULT = false;

    // ===== I18N =====
    const TRANSLATIONS = {
        en: {
            copy: '📋 COPY',
            done: '✅ DONE',
            notFound: '⚠️ NOT FOUND',
            error: '❌ ERROR',
            handleTitle: 'Drag to move · Double-click to reset position',
            logNotFound: 'Transcript not found.',
            logError: 'Copy failed:',
            menuLabel: 'Include timestamps'
        },
        es: {
            copy: '📋 COPIAR',
            done: '✅ LISTO',
            notFound: '⚠️ NO ENCONTRADO',
            error: '❌ ERROR',
            handleTitle: 'Arrastrar para mover · Doble click para resetear',
            logNotFound: 'Transcripción no encontrada.',
            logError: 'Error al copiar:',
            menuLabel: 'Incluir marcas de tiempo'
        },
        pt: {
            copy: '📋 COPIAR',
            done: '✅ PRONTO',
            notFound: '⚠️ NÃO ENCONTRADO',
            error: '❌ ERRO',
            handleTitle: 'Arrastar para mover · Duplo clique para redefinir',
            logNotFound: 'Transcrição não encontrada.',
            logError: 'Falha ao copiar:',
            menuLabel: 'Incluir marcas de tempo'
        },
        fr: {
            copy: '📋 COPIER',
            done: '✅ FAIT',
            notFound: '⚠️ INTROUVABLE',
            error: '❌ ERREUR',
            handleTitle: 'Glisser pour déplacer · Double-cliquer pour réinitialiser',
            logNotFound: 'Transcription introuvable.',
            logError: 'Échec de la copie :',
            menuLabel: 'Inclure les horodatages'
        },
        de: {
            copy: '📋 KOPIEREN',
            done: '✅ FERTIG',
            notFound: '⚠️ NICHT GEFUNDEN',
            error: '❌ FEHLER',
            handleTitle: 'Zum Verschieben ziehen · Doppelklick zum Zurücksetzen',
            logNotFound: 'Transkript nicht gefunden.',
            logError: 'Kopieren fehlgeschlagen:',
            menuLabel: 'Zeitstempel einbeziehen'
        },
        it: {
            copy: '📋 COPIA',
            done: '✅ FATTO',
            notFound: '⚠️ NON TROVATO',
            error: '❌ ERRORE',
            handleTitle: 'Trascina per spostare · Doppio clic per reimpostare',
            logNotFound: 'Trascrizione non trovata.',
            logError: 'Copia non riuscita:',
            menuLabel: 'Includi marcatori temporali'
        }
    };

    function detectLanguage() {
        const browserLang = (navigator.language || 'en').slice(0, 2).toLowerCase();
        return TRANSLATIONS[browserLang] ? browserLang : 'en';
    }

    const T = TRANSLATIONS[detectLanguage()];

    // ===== TIMESTAMP PREFERENCE =====
    const TS_PREF_KEY = 'ts-quick-copy-include-timestamps';

    function getIncludeTimestamps() {
        const stored = localStorage.getItem(TS_PREF_KEY);
        if (stored === null) return INCLUDE_TIMESTAMPS_DEFAULT;
        return stored === 'true';
    }

    // Register Tampermonkey menu command for toggling timestamps
    if (typeof GM_registerMenuCommand !== 'undefined') {
        const current = getIncludeTimestamps();
        GM_registerMenuCommand(
            `${T.menuLabel}: ${current ? 'ON' : 'OFF'}`,
            () => {
                localStorage.setItem(TS_PREF_KEY, String(!current));
                location.reload();
            }
        );
    }

    console.log(`[TS Quick Copy] Timestamps: ${getIncludeTimestamps() ? 'ON' : 'OFF'}`);

    // ===== CONFIG =====
    const DEFAULT_BOTTOM = '85px';
    const DEFAULT_RIGHT = '45px';
    const WIDGET_ID = 'ts-quick-copy-widget';

    function isTranscriptPage() {
        return location.pathname.includes('/transcript/');
    }

    function removeWidget() {
        const widget = document.getElementById(WIDGET_ID);
        if (widget) widget.remove();
    }

    // ===== TRANSCRIPT EXTRACTION =====
    function findTranscriptContainer() {
        // Primary: TurboScribe wraps transcripts in an element with id="transcript-<id>"
        let el = document.querySelector('[id^="transcript-"]');
        if (el) return el;

        // Fallback 1: common content selectors
        el = document.querySelector('article, .prose, .transcript-content');
        if (el) return el;

        // Fallback 2: any div containing (M:SS) or (MM:SS) patterns
        const timestampRegex = /\(\d+:\d{2}\)/;
        const allDivs = document.querySelectorAll('div');
        for (const div of allDivs) {
            const text = div.innerText || '';
            if (timestampRegex.test(text) && text.length > 200) {
                return div;
            }
        }
        return null;
    }

    function extractText(container) {
        // Clone so we don't modify the visible page
        const clone = container.cloneNode(true);

        if (!getIncludeTimestamps()) {
            // Remove timestamp spans directly from the cloned DOM
            clone.querySelectorAll('[data-timestamp]').forEach(el => el.remove());
        }

        let text = clone.innerText;

        if (!getIncludeTimestamps()) {
            // Belt-and-suspenders: also strip any (M:SS) survivors via regex
            text = text.replace(/\(\d+:\d{2}\)\s*/g, '');
        }

        // Whitespace cleanup
        return text
            .replace(/\n\s*\n/g, '\n\n')   // collapse 3+ blank lines to 2
            .replace(/[ \t]+/g, ' ')        // collapse multiple spaces/tabs
            .replace(/^ +| +$/gm, '')       // trim each line
            .trim();
    }

    // ===== WIDGET =====
    function createWidget() {
        if (document.getElementById(WIDGET_ID)) return;

        const container = document.createElement('div');
        container.id = WIDGET_ID;

        const posBottom = localStorage.getItem('ts-quick-copy-bottom') || DEFAULT_BOTTOM;
        const posRight = localStorage.getItem('ts-quick-copy-right') || DEFAULT_RIGHT;

        container.style.cssText = `
            position: fixed !important;
            bottom: ${posBottom} !important;
            right: ${posRight} !important;
            z-index: 2147483647 !important;
            display: flex !important;
            flex-direction: column !important;
            align-items: center !important;
            user-select: none !important;
        `;

        const handle = document.createElement('div');
        handle.innerHTML = '⋮⋮';
        handle.title = T.handleTitle;
        handle.style.cssText = `
            width: 100% !important;
            height: 12px !important;
            background-color: #dee2e6 !important;
            color: #6c757d !important;
            font-size: 8px !important;
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;
            cursor: grab !important;
            border-radius: 4px 4px 0 0 !important;
            border: 1px solid #ced4da !important;
            border-bottom: none !important;
        `;

        const btn = document.createElement('button');
        btn.innerText = T.copy;
        btn.style.cssText = `
            padding: 6px 12px !important;
            background-color: #28a745 !important;
            color: white !important;
            border: 1px solid #1e7e34 !important;
            border-radius: 0 0 4px 4px !important;
            cursor: pointer !important;
            font-size: 11px !important;
            font-weight: bold !important;
            box-shadow: 0 2px 6px rgba(0,0,0,0.2) !important;
            font-family: sans-serif !important;
            width: 100% !important;
            min-width: 110px !important;
            transition: background 0.2s !important;
            white-space: nowrap !important;
        `;

        function showFeedback(text, bgColor, borderColor, duration = 1200) {
            btn.innerText = text;
            btn.style.setProperty('background-color', bgColor, 'important');
            btn.style.setProperty('border-color', borderColor, 'important');
            setTimeout(() => {
                btn.innerText = T.copy;
                btn.style.setProperty('background-color', '#28a745', 'important');
                btn.style.setProperty('border-color', '#1e7e34', 'important');
            }, duration);
        }

        btn.onclick = async function(e) {
            e.preventDefault();

            const transcriptEl = findTranscriptContainer();
            if (!transcriptEl) {
                console.warn('[TS Quick Copy]', T.logNotFound);
                showFeedback(T.notFound, '#fd7e14', '#c2570c', 1500);
                return;
            }

            const text = extractText(transcriptEl);
            if (!text) {
                console.warn('[TS Quick Copy]', T.logNotFound);
                showFeedback(T.notFound, '#fd7e14', '#c2570c', 1500);
                return;
            }

            try {
                await navigator.clipboard.writeText(text);
                showFeedback(T.done, '#155724', '#0d3d18');
            } catch (err) {
                console.error('[TS Quick Copy]', T.logError, err);
                showFeedback(T.error, '#dc3545', '#a71d2a', 1500);
            }
        };

        // ===== DRAGGING =====
        let isDragging = false;
        let offsetX, offsetY;

        handle.onmousedown = function(e) {
            isDragging = true;
            handle.style.cursor = 'grabbing';
            offsetX = e.clientX - container.getBoundingClientRect().left;
            offsetY = e.clientY - container.getBoundingClientRect().top;
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onRelease);
            e.preventDefault();
        };

        function onMove(e) {
            if (!isDragging) return;
            let x = window.innerWidth - e.clientX - (container.offsetWidth - offsetX);
            let y = window.innerHeight - e.clientY - (container.offsetHeight - offsetY);

            x = Math.max(0, Math.min(x, window.innerWidth - container.offsetWidth));
            y = Math.max(0, Math.min(y, window.innerHeight - container.offsetHeight));

            container.style.right = x + 'px';
            container.style.bottom = y + 'px';
        }

        function onRelease() {
            isDragging = false;
            handle.style.cursor = 'grab';
            localStorage.setItem('ts-quick-copy-bottom', container.style.bottom);
            localStorage.setItem('ts-quick-copy-right', container.style.right);
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onRelease);
        }

        handle.ondblclick = function(e) {
            e.preventDefault();
            container.style.bottom = DEFAULT_BOTTOM;
            container.style.right = DEFAULT_RIGHT;
            localStorage.removeItem('ts-quick-copy-bottom');
            localStorage.removeItem('ts-quick-copy-right');
        };

        container.appendChild(handle);
        container.appendChild(btn);
        document.body.appendChild(container);
    }

    function manageWidget() {
        if (isTranscriptPage()) {
            createWidget();
        } else {
            removeWidget();
        }
    }

    setInterval(manageWidget, 1500);
})();