Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions __TEST__/e2e/interactive-export-filename.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Interactive-transcript export: pre-fill the media reference from the real
// filename of a locally-loaded file, and clear a stale value when new media is
// loaded. A local file is a blob: URL with no usable path, so previously the
// export modal's "Media file or URL" field started empty and whatever the user
// last typed (famously "test") could linger and be exported into src="…".
import { test, expect } from '@playwright/test';

// Simulate a local media load: a blob: player src (so the URL branch of
// guessMediaSrc doesn't win) plus a media file selected on a throwaway input,
// which the central capture listener reads for its name.
const loadLocalMedia = async (page, name, mimeType) => {
await page.evaluate(() => {
document.getElementById('hyperplayer').src = 'blob:http://localhost/fake-local-media';
if (!document.getElementById('__test_media_input')) {
const el = document.createElement('input');
el.type = 'file';
el.id = '__test_media_input';
document.body.appendChild(el);
}
});
await page.setInputFiles('#__test_media_input', {
name, mimeType, buffer: Buffer.from([0, 0, 0, 0]),
});
};

const openExportModalValue = (page) => page.evaluate(() => {
const modal = document.getElementById('interactive-export-modal');
modal.checked = false; // ensure the change fires on (re)open
modal.checked = true;
modal.dispatchEvent(new Event('change'));
return document.getElementById('interactive-media-filename').value;
});

test.beforeEach(async ({ page }) => {
await page.goto('/index.html');
await page.waitForSelector('#hypertranscript [data-m]');
});

test('pre-fills the export modal with a locally-loaded media filename', async ({ page }) => {
await loadLocalMedia(page, 'my-clip.mp4', 'video/mp4');
expect(await openExportModalValue(page)).toBe('my-clip.mp4');
});

test('loading new media clears a stale filename so it cannot be exported by mistake', async ({ page }) => {
await loadLocalMedia(page, 'first-take.mp4', 'video/mp4');
// user types something bogus into the field (the "test" footgun)
await page.evaluate(() => { document.getElementById('interactive-media-filename').value = 'test'; });
// loading a different clip must drop that stale value…
await loadLocalMedia(page, 'second-take.webm', 'video/webm');
expect(await page.evaluate(() => document.getElementById('interactive-media-filename').value)).toBe('');
// …and (re)opening offers the new clip's real name, not "test"
expect(await openExportModalValue(page)).toBe('second-take.webm');
});

test('non-media (import) file inputs do not overwrite the export filename', async ({ page }) => {
await loadLocalMedia(page, 'interview.mp3', 'audio/mpeg');
await page.evaluate(() => { document.getElementById('interactive-media-filename').value = 'interview.mp3'; });
// selecting a JSON/SRT/VTT import file must NOT clear or change the media name
await page.setInputFiles('#__test_media_input', {
name: 'transcript.json', mimeType: 'application/json', buffer: Buffer.from('{}'),
});
expect(await page.evaluate(() => document.getElementById('interactive-media-filename').value)).toBe('interview.mp3');
});
6 changes: 3 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- (C) The Hyperaudio Project. AGPL 3.0 @license: https://www.gnu.org/licenses/agpl-3.0.en.html -->

<!-- Hyperaudio Lite Editor - Version 0.8.9 (last changed in release 0.8.9) -->
<!-- Hyperaudio Lite Editor - Version 0.8.10 (last changed in release 0.8.10) -->

<!-- Hyperaudio Lite Editor's source code is provided under a dual license model.

Expand All @@ -18,7 +18,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="version" content="0.8.9">
<meta name="version" content="0.8.10">
<title>Hyperaudio Lite Editor</title>
<link rel="apple-touch-icon" href="images/icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
Expand Down Expand Up @@ -965,7 +965,7 @@ <h3 class="font-bold text-lg">Caption Regeneration </h3>
<script src="js/hyperaudio-lite-extension.js?v=0.7.2"></script>
<script src="js/caption.js?v=0.8.6"></script>
<script src="js/transcript-serializer.js?v=0.8.5"></script>
<script src="js/editor-core.js?v=0.8.9"></script>
<script src="js/editor-core.js?v=0.8.10"></script>


<import-deepgram-json></import-deepgram-json>
Expand Down
26 changes: 22 additions & 4 deletions js/editor-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,33 @@
const iaModal = document.getElementById('interactive-export-modal');
const iaInput = document.getElementById('interactive-media-filename');
const iaDownload = document.getElementById('interactive-export-download');
let lastLoadedMediaName = '';

// Remote media can be linked by its URL as-is; a blob:/data: source (local
// upload or Recents) has no usable path, so the field starts empty for the
// user to type the local filename.
// Remote media can be linked by its URL as-is. A blob:/data: source (local
// upload) has no usable path, so we fall back to the real filename captured
// when the file was loaded (below) — which lets the field pre-fill for local
// files instead of forcing the user to type the reference by hand.
const guessMediaSrc = () => {
const src = document.querySelector('#hyperplayer') ? document.querySelector('#hyperplayer').src : '';
return /^https?:/i.test(src) ? src : '';
return /^https?:/i.test(src) ? src : lastLoadedMediaName;
};

// Capture the real filename of a locally-loaded media file so guessMediaSrc
// can offer it (a blob: URL carries no name). Read centrally from any media
// file input; import inputs (JSON/SRT/VTT) are skipped by the media check.
// Loading new media also clears the field, so a name typed for the PREVIOUS
// clip can't linger and be exported by mistake (the src="test" footgun).
const MEDIA_EXTENSION = /\.(mp4|webm|ogv|ogg|mov|m4v|mkv|mp3|m4a|wav|aac|flac|opus)$/i;
document.addEventListener('change', (e) => {
const input = e.target;
if (!input || input.type !== 'file' || !input.files || input.files.length === 0) return;
const file = input.files[0];
const isMedia = /^(audio|video)\//i.test(file.type || '') || MEDIA_EXTENSION.test(file.name);
if (!isMedia) return;
lastLoadedMediaName = file.name;
if (iaInput !== null) iaInput.value = '';
}, true);

if (iaModal !== null && iaInput !== null) {
iaModal.addEventListener('change', () => {
if (iaModal.checked && iaInput.value.trim() === '') {
Expand Down
Loading