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.
// ==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);
})();