🪻 distributed transcription service thistle.dunkirk.sh

feat: add copy button

dunkirk.sh c436bc56 59f82f0d

verified
Changed files
+70 -2
src
components
+70 -2
src/components/vtt-viewer.ts
···
@property({ type: String }) audioId = "";
static override styles = css`
.transcript {
background: color-mix(in srgb, var(--primary) 5%, transparent);
border-radius: 6px;
···
if (!audioElement || !transcriptDiv) return;
// Clear any lingering highlights from prior instances
-
transcriptDiv.querySelectorAll('.current-segment').forEach((el) => (el as HTMLElement).classList.remove('current-segment'));
this.audioElement = audioElement;
let currentSegmentElement: HTMLElement | null = null;
···
return html`${paragraphs}`;
}
override render() {
-
return html`<div class="transcript">${this.renderFromVTT()}</div>`;
}
}
···
@property({ type: String }) audioId = "";
static override styles = css`
+
.viewer-container {
+
position: relative;
+
}
+
+
.copy-btn {
+
position: absolute;
+
top: 0.5rem;
+
right: 0.5rem;
+
background: var(--primary);
+
color: var(--background);
+
border: none;
+
padding: 0.25rem 0.5rem;
+
border-radius: 4px;
+
font-size: 0.875rem;
+
cursor: pointer;
+
opacity: 0;
+
transition: opacity 0.15s ease;
+
}
+
+
.viewer-container:hover .copy-btn {
+
opacity: 1;
+
}
+
.transcript {
background: color-mix(in srgb, var(--primary) 5%, transparent);
border-radius: 6px;
···
if (!audioElement || !transcriptDiv) return;
// Clear any lingering highlights from prior instances
+
transcriptDiv.querySelectorAll('.current-segment').forEach((el) => { (el as HTMLElement).classList.remove('current-segment'); });
this.audioElement = audioElement;
let currentSegmentElement: HTMLElement | null = null;
···
return html`${paragraphs}`;
}
+
private extractPlainText(): string {
+
if (!this.vttContent) return "";
+
const segments = parseVTT(this.vttContent);
+
// Group into paragraphs by index as in renderFromVTT
+
const paragraphGroups = new Map<string, string[]>();
+
for (const s of segments) {
+
const id = (s.index || "").trim();
+
const match = id.match(/^Paragraph\s+(\d+)-/);
+
const paraNum = match ? match[1] : '0';
+
if (!paragraphGroups.has(paraNum)) paragraphGroups.set(paraNum, []);
+
paragraphGroups.get(paraNum)!.push(s.text || '');
+
}
+
const paragraphs = Array.from(paragraphGroups.values()).map(group => group.join(' ').replace(/\s+/g, ' ').trim());
+
return paragraphs.join('\n\n').trim();
+
}
+
+
private async copyTranscript(e?: Event) {
+
e && e.stopPropagation();
+
const text = this.extractPlainText();
+
if (!text) return;
+
try {
+
if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) {
+
await (navigator as any).clipboard.writeText(text);
+
} else {
+
// Fallback
+
const ta = document.createElement('textarea');
+
ta.value = text;
+
ta.style.position = 'fixed';
+
ta.style.opacity = '0';
+
document.body.appendChild(ta);
+
ta.select();
+
document.execCommand('copy');
+
document.body.removeChild(ta);
+
}
+
const btn = this.shadowRoot?.querySelector('.copy-btn') as HTMLButtonElement | null;
+
if (btn) {
+
const orig = btn.innerText;
+
btn.innerText = 'Copied!';
+
setTimeout(() => { btn.innerText = orig; }, 1500);
+
}
+
} catch {
+
// ignore
+
}
+
}
+
override render() {
+
return html`<div class="viewer-container"><button class="copy-btn" @click=${this.copyTranscript} aria-label="Copy transcript">Copy</button><div class="transcript">${this.renderFromVTT()}</div></div>`;
}
}