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.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();