Reorderable columns, a loaded first file, and a spectrogram worth keeping
Drag a heading and the column moves, with a copy of it following the pointer and the column it came from stepping back. One array of keys — every column, shown or hidden — backs the table, the Settings list and what is saved, so a hidden column keeps its neighbour and returns where it was rather than at the end. A drag ends in a click, and the click is what sorting listens for, so that click is swallowed. Opening a folder loads its first file: the read, the peaks, the channel chips and the waveform, then paused at the start rather than playing. playRow gained a paused mode instead of a second loader that could drift from it. It cannot stop a folder opening, and a file that will not decode no longer writes a playback error over the line saying the folder opened — nobody asked it to play. The badge follows the transport now, so a paused file stops claiming to be playing. A bulk run re-reads every file it touches, but nothing told the transport to look again: rename the boom across a card and the mixer went on showing the old name for the very file it had just rewritten. Settings loses its Done button and the mixer its Close — both apply as you touch them, and the window's own close is enough. Save Image moves onto the file name's line; the rule meant to do that had been dead for some time, overwritten by the shared modal header. The exported spectrogram carries the folder, the file and the frequency axis, drawn in, because a PNG read somewhere else has none of that. The viewer is unchanged. Mixer track names wear the player's pill, the waveform thumbnails give their grey panel back to the row, and every button is the transport's size. Build output that had been committed by accident is untracked, and .gitignore grows the entries for it. 459 checks.
This commit is contained in:
+523
-1
@@ -1566,6 +1566,9 @@ SPECTRO_PATCHES = [
|
||||
\t\tvar spectroNote = container.querySelector( "[data-bwfa-spectro-note]" );
|
||||
\t\tvar spectroTitle = container.querySelector( "[data-bwfa-spectro-title]" );
|
||||
\t\tvar spectroScale = container.querySelector( "[data-bwfa-spectro-scale]" );
|
||||
\t\t// Kept so the exported image can label its own frequency axis. On
|
||||
\t\t// screen the axis is HTML beside the canvas; a PNG has to carry it.
|
||||
\t\tvar spectroPicture = null;
|
||||
|
||||
\t\t/**
|
||||
\t\t * Viridis, sampled. Perceptually even, so a bright patch means a loud
|
||||
@@ -1602,6 +1605,7 @@ SPECTRO_PATCHES = [
|
||||
\t\t\tif ( ! ctx ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tspectroPicture = picture;
|
||||
\t\t\tspectroCanvas.width = picture.columns;
|
||||
\t\t\tspectroCanvas.height = picture.bins;
|
||||
\t\t\tvar image = ctx.createImageData( picture.columns, picture.bins );
|
||||
@@ -1636,6 +1640,81 @@ SPECTRO_PATCHES = [
|
||||
\t\t\t} );
|
||||
\t\t}
|
||||
|
||||
\t\t/**
|
||||
\t\t * The picture as something worth keeping: the file it came from, the
|
||||
\t\t * reading underneath it, and the frequency axis, drawn into the image
|
||||
\t\t * rather than sitting beside it in HTML.
|
||||
\t\t *
|
||||
\t\t * A PNG leaves the app and is looked at somewhere else, where none of
|
||||
\t\t * that context survives — a bare spectrogram is a pretty picture of an
|
||||
\t\t * unknown file at an unknown scale. On screen it is all still there in
|
||||
\t\t * the modal, so the viewer keeps exactly the layout it has.
|
||||
\t\t *
|
||||
\t\t * Black on white regardless of the app's own theme: this is a figure to
|
||||
\t\t * be sent, printed or dropped into a report.
|
||||
\t\t */
|
||||
\t\tfunction buildSpectroExport( row ) {
|
||||
\t\t\tif ( ! spectroPicture || ! spectroCanvas.width ) {
|
||||
\t\t\t\treturn null;
|
||||
\t\t\t}
|
||||
\t\t\tvar gutter = 74;
|
||||
\t\t\tvar header = 54;
|
||||
\t\t\tvar pad = 14;
|
||||
\t\t\tvar plotWidth = spectroCanvas.width;
|
||||
\t\t\t// The bins are far shorter than the columns, and the modal stretches
|
||||
\t\t\t// them to fit. Do the same rather than exporting a 1200×512 sliver.
|
||||
\t\t\tvar plotHeight = 460;
|
||||
|
||||
\t\t\tvar out = document.createElement( "canvas" );
|
||||
\t\t\tout.width = gutter + plotWidth + pad;
|
||||
\t\t\tout.height = header + plotHeight + pad;
|
||||
\t\t\tvar ctx = out.getContext( "2d" );
|
||||
\t\t\tif ( ! ctx ) {
|
||||
\t\t\t\treturn null;
|
||||
\t\t\t}
|
||||
\t\t\tvar sans = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif';
|
||||
|
||||
\t\t\tctx.fillStyle = "#ffffff";
|
||||
\t\t\tctx.fillRect( 0, 0, out.width, out.height );
|
||||
|
||||
\t\t\t// The folder as well as the file: a take number on its own names
|
||||
\t\t\t// nothing once the picture is out of the app.
|
||||
\t\t\tvar folder = window.BWFA_FOLDER_NAME || "";
|
||||
\t\t\tvar fileName = ( row && row.parsed && row.parsed.fileName ) || "";
|
||||
\t\t\tctx.fillStyle = "#111111";
|
||||
\t\t\tctx.font = "600 19px " + sans;
|
||||
\t\t\tctx.textBaseline = "alphabetic";
|
||||
\t\t\tctx.fillText( folder ? ( folder + " / " + fileName ) : fileName, gutter, 26 );
|
||||
\t\t\tctx.fillStyle = "#555555";
|
||||
\t\t\tctx.font = "13px " + sans;
|
||||
\t\t\tctx.fillText( spectroNote.textContent || "", gutter, 45 );
|
||||
|
||||
\t\t\t// The picture itself, unsmoothed for the same reason the modal
|
||||
\t\t\t// doesn't smooth it: it would invent detail the file doesn't have.
|
||||
\t\t\tctx.imageSmoothingEnabled = false;
|
||||
\t\t\tctx.drawImage( spectroCanvas, gutter, header, plotWidth, plotHeight );
|
||||
|
||||
\t\t\t// And the axis it is read against, on the same five marks the modal
|
||||
\t\t\t// shows: Nyquist at the top, DC at the bottom.
|
||||
\t\t\tvar top = spectroPicture.sampleRate / 2;
|
||||
\t\t\tctx.fillStyle = "#555555";
|
||||
\t\t\tctx.font = "12px " + sans;
|
||||
\t\t\tctx.textAlign = "right";
|
||||
\t\t\t[ 1, 0.75, 0.5, 0.25, 0 ].forEach( function ( part ) {
|
||||
\t\t\t\tvar hz = top * part;
|
||||
\t\t\t\tvar label = hz >= 1000
|
||||
\t\t\t\t\t? ( Math.round( hz / 100 ) / 10 ) + " kHz"
|
||||
\t\t\t\t\t: Math.round( hz ) + " Hz";
|
||||
\t\t\t\tvar y = header + ( 1 - part ) * plotHeight;
|
||||
\t\t\t\t// Nudged inside the plot at the ends so neither mark is clipped.
|
||||
\t\t\t\tvar baseline = Math.max( header + 10, Math.min( header + plotHeight, y + 4 ) );
|
||||
\t\t\t\tctx.fillText( label, gutter - 10, baseline );
|
||||
\t\t\t} );
|
||||
\t\t\tctx.textAlign = "left";
|
||||
|
||||
\t\t\treturn out;
|
||||
\t\t}
|
||||
|
||||
\t\tfunction openSpectro() {
|
||||
\t\t\tvar row = playerRow();
|
||||
\t\t\tif ( ! row || ! row.file ) {
|
||||
@@ -1682,7 +1761,7 @@ SPECTRO_PATCHES = [
|
||||
\t\t\t\tvar row = playerRow();
|
||||
\t\t\t\tvar stem = row && row.parsed
|
||||
\t\t\t\t\t? row.parsed.fileName.replace( /\\.[^.]*$/, "" ) : "spectrogram";
|
||||
\t\t\t\tspectroCanvas.toBlob( function ( blob ) {
|
||||
\t\t\t\t( buildSpectroExport( row ) || spectroCanvas ).toBlob( function ( blob ) {
|
||||
\t\t\t\t\tif ( ! blob ) {
|
||||
\t\t\t\t\t\treturn;
|
||||
\t\t\t\t\t}
|
||||
@@ -2584,7 +2663,448 @@ PLAYER_PATCHES = [
|
||||
),
|
||||
]
|
||||
|
||||
# ---- the order of the columns belongs to whoever is looking ------------
|
||||
#
|
||||
# Drag a heading and the column moves. Applied last, so its anchors are the
|
||||
# markup as every other patch leaves it.
|
||||
#
|
||||
# Three things have to agree: the table, the Settings list and what is on
|
||||
# disk. All three read one array of keys — every column, shown or hidden —
|
||||
# so there is no second copy to fall out of step.
|
||||
COLUMN_ORDER_PATCHES = [
|
||||
(
|
||||
"""\tfunction getDefaultVisibleColumnKeys( columns ) {""",
|
||||
"""\tvar COLUMN_ORDER_STORAGE_KEY = "bwfa_column_order_v1";
|
||||
|
||||
\t/**
|
||||
\t * The order the columns are shown in, as a list of keys.
|
||||
\t *
|
||||
\t * Kept for every column, hidden ones included. Switching a column off and
|
||||
\t * on again should put it back where it was rather than at the end, and
|
||||
\t * dragging past a hidden column should not shunt it elsewhere.
|
||||
\t */
|
||||
\tfunction loadColumnOrder( columns, storage ) {
|
||||
\t\tvar defaults = columns.map( function ( col ) { return col.key; } );
|
||||
\t\tif ( ! storage ) {
|
||||
\t\t\treturn defaults;
|
||||
\t\t}
|
||||
\t\ttry {
|
||||
\t\t\tvar parsed = JSON.parse( storage.getItem( COLUMN_ORDER_STORAGE_KEY ) || "null" );
|
||||
\t\t\tif ( ! Array.isArray( parsed ) ) {
|
||||
\t\t\t\treturn defaults;
|
||||
\t\t\t}
|
||||
\t\t\t// A key that is no longer a column is dropped; a column added to
|
||||
\t\t\t// the app since this was saved is slotted in where it ships, rather
|
||||
\t\t\t// than appearing at the far end of somebody's saved order.
|
||||
\t\t\tvar kept = parsed.filter( function ( key ) {
|
||||
\t\t\t\treturn defaults.indexOf( key ) !== -1;
|
||||
\t\t\t} );
|
||||
\t\t\tdefaults.forEach( function ( key, index ) {
|
||||
\t\t\t\tif ( kept.indexOf( key ) === -1 ) {
|
||||
\t\t\t\t\tkept.splice( Math.min( index, kept.length ), 0, key );
|
||||
\t\t\t\t}
|
||||
\t\t\t} );
|
||||
\t\t\treturn kept;
|
||||
\t\t} catch ( e ) {
|
||||
\t\t\treturn defaults;
|
||||
\t\t}
|
||||
\t}
|
||||
|
||||
\tfunction saveColumnOrder( order, storage ) {
|
||||
\t\tif ( ! storage ) {
|
||||
\t\t\treturn;
|
||||
\t\t}
|
||||
\t\ttry {
|
||||
\t\t\tstorage.setItem( COLUMN_ORDER_STORAGE_KEY, JSON.stringify( order ) );
|
||||
\t\t} catch ( e ) {
|
||||
\t\t\t// As with the visible-column picker: it works for this session and
|
||||
\t\t\t// simply isn't remembered. Not fatal.
|
||||
\t\t}
|
||||
\t}
|
||||
|
||||
\tfunction getDefaultVisibleColumnKeys( columns ) {""",
|
||||
),
|
||||
(
|
||||
"""\t\t\tvisibleColumns: loadVisibleColumns( TABLE_COLUMNS, storage ),
|
||||
\t\t\texportFields: loadExportFields( storage ),""",
|
||||
"""\t\t\tvisibleColumns: loadVisibleColumns( TABLE_COLUMNS, storage ),
|
||||
\t\t\tcolumnOrder: loadColumnOrder( TABLE_COLUMNS, storage ),
|
||||
\t\t\texportFields: loadExportFields( storage ),""",
|
||||
),
|
||||
(
|
||||
"""\t\tfunction visibleColumnList() {
|
||||
\t\t\treturn TABLE_COLUMNS.filter( function ( col ) { return state.visibleColumns.has( col.key ); } );
|
||||
\t\t}""",
|
||||
"""\t\t/** Every column there is, in the order they are shown. */
|
||||
\t\tfunction orderedColumnList() {
|
||||
\t\t\tvar byKey = {};
|
||||
\t\t\tTABLE_COLUMNS.forEach( function ( col ) { byKey[ col.key ] = col; } );
|
||||
\t\t\treturn ( state.columnOrder || [] ).map( function ( key ) {
|
||||
\t\t\t\treturn byKey[ key ];
|
||||
\t\t\t} ).filter( Boolean );
|
||||
\t\t}
|
||||
|
||||
\t\tfunction visibleColumnList() {
|
||||
\t\t\treturn orderedColumnList().filter( function ( col ) {
|
||||
\t\t\t\treturn state.visibleColumns.has( col.key );
|
||||
\t\t\t} );
|
||||
\t\t}
|
||||
|
||||
\t\t/**
|
||||
\t\t * Moves a column so it sits immediately before another, or to the end
|
||||
\t\t * when there is nothing to sit before.
|
||||
\t\t *
|
||||
\t\t * Both are keys, not positions, and the list being reordered is the
|
||||
\t\t * full one. A hidden column keeps whichever neighbour it already had,
|
||||
\t\t * so switching it back on puts it somewhere recognisable.
|
||||
\t\t */
|
||||
\t\tfunction moveColumn( key, beforeKey ) {
|
||||
\t\t\tvar order = ( state.columnOrder || [] ).slice();
|
||||
\t\t\tvar from = order.indexOf( key );
|
||||
\t\t\tif ( from === -1 ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\torder.splice( from, 1 );
|
||||
\t\t\tvar at = beforeKey ? order.indexOf( beforeKey ) : -1;
|
||||
\t\t\tif ( at === -1 ) {
|
||||
\t\t\t\torder.push( key );
|
||||
\t\t\t} else {
|
||||
\t\t\t\torder.splice( at, 0, key );
|
||||
\t\t\t}
|
||||
\t\t\tstate.columnOrder = order;
|
||||
\t\t\tsaveColumnOrder( order, storage );
|
||||
\t\t}""",
|
||||
),
|
||||
(
|
||||
"""\t\tfunction renderTableHead() {""",
|
||||
"""\t\t/* ---- dragging a heading ----------------------------------------
|
||||
\t\t Pointer events, not HTML5 drag-and-drop. The window has Tauri's
|
||||
\t\t native drag-drop switched on so a folder can be dropped onto it,
|
||||
\t\t and on macOS that intercepts the webview's own drag events.
|
||||
|
||||
\t\t It suits the table anyway: no drag image, no insertion line, no
|
||||
\t\t placeholder, nothing added to the page at any point. The columns
|
||||
\t\t themselves move as the pointer passes them, which is the only
|
||||
\t\t feedback there is and the only kind that costs the design nothing.
|
||||
\t\t -------------------------------------------------------------- */
|
||||
|
||||
\t\tvar columnDrag = null;
|
||||
\t\tvar sortClickSuppressed = false;
|
||||
|
||||
\t\t/** The key of the heading under this x position, if any. */
|
||||
\t\tfunction columnKeyAt( clientX ) {
|
||||
\t\t\tvar found = null;
|
||||
\t\t\ttableHead.querySelectorAll( "th[data-bwfa-col]" ).forEach( function ( th ) {
|
||||
\t\t\t\tvar box = th.getBoundingClientRect();
|
||||
\t\t\t\tif ( clientX >= box.left && clientX <= box.right ) {
|
||||
\t\t\t\t\tfound = th.getAttribute( "data-bwfa-col" );
|
||||
\t\t\t\t}
|
||||
\t\t\t} );
|
||||
\t\t\treturn found;
|
||||
\t\t}
|
||||
|
||||
\t\tfunction startColumnDrag( e, key, label ) {
|
||||
\t\t\tif ( e.button !== 0 ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tcolumnDrag = { key: key, label: label, x: e.clientX, moved: false, ghost: null };
|
||||
\t\t}
|
||||
|
||||
\t\t/** The heading you are holding, following the pointer. */
|
||||
\t\tfunction showColumnGhost( e ) {
|
||||
\t\t\tif ( ! columnDrag.ghost ) {
|
||||
\t\t\t\tvar ghost = document.createElement( "div" );
|
||||
\t\t\t\tghost.className = "bwfa-col-ghost";
|
||||
\t\t\t\tghost.setAttribute( "data-bwfa-col-ghost", columnDrag.key );
|
||||
\t\t\t\tghost.textContent = columnDrag.label;
|
||||
\t\t\t\tcontainer.appendChild( ghost );
|
||||
\t\t\t\tcolumnDrag.ghost = ghost;
|
||||
\t\t\t}
|
||||
\t\t\tcolumnDrag.ghost.style.left = ( e.clientX + 12 ) + "px";
|
||||
\t\t\tcolumnDrag.ghost.style.top = ( e.clientY + 12 ) + "px";
|
||||
\t\t\t// Re-applied after every reorder, because the heading it marks is a
|
||||
\t\t\t// new element by then.
|
||||
\t\t\ttableHead.querySelectorAll( "th[data-bwfa-col]" ).forEach( function ( th ) {
|
||||
\t\t\t\tth.classList.toggle( "is-dragging",
|
||||
\t\t\t\t\tth.getAttribute( "data-bwfa-col" ) === columnDrag.key );
|
||||
\t\t\t} );
|
||||
\t\t}
|
||||
|
||||
\t\tfunction endColumnGhost() {
|
||||
\t\t\tif ( columnDrag && columnDrag.ghost && columnDrag.ghost.parentNode ) {
|
||||
\t\t\t\tcolumnDrag.ghost.parentNode.removeChild( columnDrag.ghost );
|
||||
\t\t\t}
|
||||
\t\t\ttableHead.querySelectorAll( "th.is-dragging" ).forEach( function ( th ) {
|
||||
\t\t\t\tth.classList.remove( "is-dragging" );
|
||||
\t\t\t} );
|
||||
\t\t}
|
||||
|
||||
\t\tdocument.addEventListener( "mousemove", function ( e ) {
|
||||
\t\t\tif ( ! columnDrag ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\t// A few pixels of slack, so a click with a tremor in it still sorts.
|
||||
\t\t\tif ( ! columnDrag.moved && Math.abs( e.clientX - columnDrag.x ) < 4 ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tcolumnDrag.moved = true;
|
||||
\t\t\tshowColumnGhost( e );
|
||||
\t\t\tvar over = columnKeyAt( e.clientX );
|
||||
\t\t\tif ( ! over || over === columnDrag.key ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tvar visible = visibleColumnList().map( function ( col ) { return col.key; } );
|
||||
\t\t\tvar from = visible.indexOf( columnDrag.key );
|
||||
\t\t\tvar to = visible.indexOf( over );
|
||||
\t\t\tif ( from === -1 || to === -1 ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\t// Rightwards it lands after the heading it passed, leftwards before
|
||||
\t\t\t// it — so the one under the pointer stays under the pointer.
|
||||
\t\t\tmoveColumn( columnDrag.key, to > from ? ( visible[ to + 1 ] || null ) : over );
|
||||
\t\t\trenderTableHead();
|
||||
\t\t\trenderTableBody();
|
||||
\t\t\trenderColumnsMenu();
|
||||
\t\t\t// The head was just rebuilt, so the heading being dragged has to be
|
||||
\t\t\t// marked again on its new element.
|
||||
\t\t\tshowColumnGhost( e );
|
||||
\t\t} );
|
||||
|
||||
\t\tdocument.addEventListener( "mouseup", function () {
|
||||
\t\t\tif ( ! columnDrag ) {
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\tendColumnGhost();
|
||||
\t\t\t// The click that ends a drag is the end of the drag, not a request
|
||||
\t\t\t// to sort by whatever the pointer came to rest on.
|
||||
\t\t\tsortClickSuppressed = columnDrag.moved;
|
||||
\t\t\tcolumnDrag = null;
|
||||
\t\t\tif ( sortClickSuppressed ) {
|
||||
\t\t\t\twindow.setTimeout( function () { sortClickSuppressed = false; }, 0 );
|
||||
\t\t\t}
|
||||
\t\t} );
|
||||
|
||||
\t\tfunction renderTableHead() {""",
|
||||
),
|
||||
(
|
||||
"""\t\t\t\tvar th = document.createElement( "th" );
|
||||
\t\t\t\tth.textContent = col.label;
|
||||
\t\t\t\tif ( col.sortable ) {
|
||||
\t\t\t\t\tth.setAttribute( "data-bwfa-sort", col.key );
|
||||
\t\t\t\t\tvar indicator = el( "span", "bwfa-sort-indicator", "\\u25B2" );
|
||||
\t\t\t\t\tth.appendChild( indicator );
|
||||
\t\t\t\t\tth.addEventListener( "click", function () {
|
||||
\t\t\t\t\t\tif ( state.sortKey === col.key ) {""",
|
||||
"""\t\t\t\tvar th = document.createElement( "th" );
|
||||
\t\t\t\tth.textContent = col.label;
|
||||
\t\t\t\t// Invisible: somewhere for the drag to take hold, and how the
|
||||
\t\t\t\t// pointer is matched to a heading. Every column can be dragged,
|
||||
\t\t\t\t// including the two that cannot be sorted.
|
||||
\t\t\t\tth.setAttribute( "data-bwfa-col", col.key );
|
||||
\t\t\t\tth.addEventListener( "mousedown", function ( e ) {
|
||||
\t\t\t\t\tstartColumnDrag( e, col.key, col.label );
|
||||
\t\t\t\t} );
|
||||
\t\t\t\tif ( col.sortable ) {
|
||||
\t\t\t\t\tth.setAttribute( "data-bwfa-sort", col.key );
|
||||
\t\t\t\t\tvar indicator = el( "span", "bwfa-sort-indicator", "\\u25B2" );
|
||||
\t\t\t\t\tth.appendChild( indicator );
|
||||
\t\t\t\t\tth.addEventListener( "click", function () {
|
||||
\t\t\t\t\t\tif ( sortClickSuppressed ) {
|
||||
\t\t\t\t\t\t\treturn;
|
||||
\t\t\t\t\t\t}
|
||||
\t\t\t\t\t\tif ( state.sortKey === col.key ) {""",
|
||||
),
|
||||
(
|
||||
"""\t\t\tcolumnsMenu.textContent = "";
|
||||
\t\t\tTABLE_COLUMNS.forEach( function ( col ) {""",
|
||||
"""\t\t\tcolumnsMenu.textContent = "";
|
||||
\t\t\t// In the order the table shows them, so the list reads like the
|
||||
\t\t\t// table rather than like the source file.
|
||||
\t\t\torderedColumnList().forEach( function ( col ) {""",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---- a folder that opens has its first file on the transport -----------
|
||||
FIRST_FILE_PATCHES = [
|
||||
# playRow gains a paused mode: everything it does to load a file, minus
|
||||
# the two lines that make a sound. Loading is the expensive, fiddly part
|
||||
# — the read, the peaks, the graph, the channel chips — and a second
|
||||
# copy of it that drifted from this one is exactly the bug that would
|
||||
# not show up until someone pressed Play on a primed file.
|
||||
(
|
||||
"""\t\tfunction playRow( row ) {""",
|
||||
"""\t\tfunction playRow( row, startPaused ) {""",
|
||||
),
|
||||
(
|
||||
"""\t\t\tvar playingBadge = container.querySelector( "[data-bwfa-player-badge]" );
|
||||
\t\t\tif ( playingBadge ) {
|
||||
\t\t\t\tplayingBadge.textContent = t( "playerPlaying" );
|
||||
\t\t\t}""",
|
||||
"""\t\t\tvar playingBadge = container.querySelector( "[data-bwfa-player-badge]" );
|
||||
\t\t\tif ( playingBadge ) {
|
||||
\t\t\t\tplayingBadge.textContent = startPaused ? t( "playerReady" ) : t( "playerPlaying" );
|
||||
\t\t\t}""",
|
||||
),
|
||||
(
|
||||
"""\t\t\t\t\tisPlaying: true,""",
|
||||
"""\t\t\t\t\tisPlaying: ! startPaused,""",
|
||||
),
|
||||
# Nobody asked the primed file to play, so a file that won't load must
|
||||
# not shout about it. handlePlaybackError writes to the status line, and
|
||||
# the status line has just said the folder opened — replacing that with
|
||||
# "this file could not be played back" reads as the folder having failed.
|
||||
(
|
||||
"""\t\t\t} ).catch( function () {
|
||||
\t\t\t\tplayerDecoding.hidden = true;
|
||||
\t\t\t\thandlePlaybackError();
|
||||
\t\t\t\tshowIdlePlayer( null );
|
||||
\t\t\t} );""",
|
||||
"""\t\t\t} ).catch( function () {
|
||||
\t\t\t\tplayerDecoding.hidden = true;
|
||||
\t\t\t\tif ( ! startPaused ) {
|
||||
\t\t\t\t\thandlePlaybackError();
|
||||
\t\t\t\t}
|
||||
\t\t\t\tshowIdlePlayer( null );
|
||||
\t\t\t} );""",
|
||||
),
|
||||
# The badge was written once, when a file was started, and never again —
|
||||
# so a file paused from the transport still read "Now playing". Loading a
|
||||
# file up front made that plain: it announced itself as playing the
|
||||
# moment you pressed Play on it, because the word never changed. It
|
||||
# belongs with everything else the transport keeps in step.
|
||||
(
|
||||
"""\t\tfunction syncPlaybackUI() {
|
||||
\t\t\tsyncMixerTransport();
|
||||
\t\t\tsyncMixerStates();""",
|
||||
"""\t\tfunction syncPlaybackUI() {
|
||||
\t\t\tsyncMixerTransport();
|
||||
\t\t\tsyncMixerStates();
|
||||
\t\t\tvar transportBadge = container.querySelector( "[data-bwfa-player-badge]" );
|
||||
\t\t\tif ( transportBadge && state.playback ) {
|
||||
\t\t\t\ttransportBadge.textContent = state.playback.isPlaying
|
||||
\t\t\t\t\t? t( "playerPlaying" ) : t( "playerReady" );
|
||||
\t\t\t}""",
|
||||
),
|
||||
(
|
||||
"""\t\t\tstate.playback.elapsed = 0;
|
||||
\t\t\t\tplayer( "bwf_play", {
|
||||
\t\t\t\t\tpath: row.file.path,
|
||||
\t\t\t\t\toffset: startAt,
|
||||
\t\t\t\t\tgains: state.playback.channelGains || []
|
||||
\t\t\t\t} ).catch( function () { handlePlaybackError(); } );
|
||||
\t\t\t\trenderChannelChips( row );
|
||||
\t\t\t\tfitCanvasResolution( playerWaveformCanvas );
|
||||
\t\t\t\tstartTransportLoop();
|
||||
\t\t\t\tsyncPlaybackUI();""",
|
||||
"""\t\t\tstate.playback.elapsed = 0;
|
||||
\t\t\t\tif ( ! startPaused ) {
|
||||
\t\t\t\t\tplayer( "bwf_play", {
|
||||
\t\t\t\t\t\tpath: row.file.path,
|
||||
\t\t\t\t\t\toffset: startAt,
|
||||
\t\t\t\t\t\tgains: state.playback.channelGains || []
|
||||
\t\t\t\t\t} ).catch( function () { handlePlaybackError(); } );
|
||||
\t\t\t\t}
|
||||
\t\t\t\trenderChannelChips( row );
|
||||
\t\t\t\tfitCanvasResolution( playerWaveformCanvas );
|
||||
\t\t\t\tif ( startPaused ) {
|
||||
\t\t\t\t\t// No loop to draw the first frame, so draw it: the waveform and
|
||||
\t\t\t\t\t// the length are the point of loading it up front.
|
||||
\t\t\t\t\tupdateTransportDisplay();
|
||||
\t\t\t\t} else {
|
||||
\t\t\t\t\tstartTransportLoop();
|
||||
\t\t\t\t}
|
||||
\t\t\t\tsyncPlaybackUI();""",
|
||||
),
|
||||
(
|
||||
"""\t\tfunction setEditableMode( editable ) {""",
|
||||
"""\t\t/**
|
||||
\t\t * Puts the first file of the folder on the transport, ready to play.
|
||||
\t\t *
|
||||
\t\t * Ready, not playing: opening a folder is not a request to make a
|
||||
\t\t * noise. The transport's idle path already knows how to start what it
|
||||
\t\t * is holding, so Play and the spacebar both work from here without
|
||||
\t\t * anything else being wired up.
|
||||
\t\t *
|
||||
\t\t * Runs on every folder load — the first of the day, the one reopened
|
||||
\t\t * at launch, and any folder chosen after that — so the transport names
|
||||
\t\t * the folder on screen rather than sitting on a file from the last one
|
||||
\t\t * that is no longer in the table.
|
||||
\t\t */
|
||||
\t\tfunction primePlayerWithFirstRow() {
|
||||
\t\t\tvar first = state.filteredRows[ 0 ] || null;
|
||||
\t\t\tif ( ! first ) {
|
||||
\t\t\t\tstate.lastPlayed = null;
|
||||
\t\t\t\tplayerWrap.hidden = true;
|
||||
\t\t\t\treturn;
|
||||
\t\t\t}
|
||||
\t\t\t// The whole load, not a label: the file is read, the waveform drawn
|
||||
\t\t\t// and the channels listed, exactly as pressing Play would — and then
|
||||
\t\t\t// it sits there paused at the start instead of making a sound.
|
||||
\t\t\t//
|
||||
\t\t\t// Contained, because this is a convenience on top of opening a
|
||||
\t\t\t// folder and must never be able to stop one opening. A file that
|
||||
\t\t\t// won't load — a corrupt header, an engine that isn't answering —
|
||||
\t\t\t// leaves the table exactly as it is and the transport merely idle.
|
||||
\t\t\ttry {
|
||||
\t\t\t\tplayRow( first, true );
|
||||
\t\t\t} catch ( e ) {
|
||||
\t\t\t\tconsole.error( "BWF Analyser: the folder's first file could not be" +
|
||||
\t\t\t\t\t" loaded into the transport — " + ( ( e && e.message ) || e ) );
|
||||
\t\t\t\tstate.lastPlayed = { row: first, peaks: null, duration: 0 };
|
||||
\t\t\t\tplayerWrap.hidden = false;
|
||||
\t\t\t\tshowIdlePlayer( state.lastPlayed );
|
||||
\t\t\t}
|
||||
\t\t}
|
||||
|
||||
\t\tfunction setEditableMode( editable ) {""",
|
||||
),
|
||||
# The scan itself starts from nothing on the transport. Without this the
|
||||
# previous folder's file sits there, named, for as long as the new folder
|
||||
# takes to read — which on a card is long enough to look like the answer.
|
||||
(
|
||||
"""\t\t\tstate.rows = [];
|
||||
\t\t\tstopPlayback();
|
||||
\t\t\tstate.decodedBufferCache.clear();""",
|
||||
"""\t\t\tstate.rows = [];
|
||||
\t\t\tstopPlayback();
|
||||
\t\t\tstate.lastPlayed = null;
|
||||
\t\t\tstate.decodedBufferCache.clear();""",
|
||||
),
|
||||
(
|
||||
"""\t\t\t\tstate.rows = results;
|
||||
\t\t\t\tsetProgress( 0, 0 );
|
||||
\t\t\t\tsetStatus( format( t( "statusDone" ), results.length, skipped ) );
|
||||
\t\t\t\tapplyFilterAndSort();""",
|
||||
"""\t\t\t\tstate.rows = results;
|
||||
\t\t\t\tsetProgress( 0, 0 );
|
||||
\t\t\t\tsetStatus( format( t( "statusDone" ), results.length, skipped ) );
|
||||
\t\t\t\tapplyFilterAndSort();
|
||||
\t\t\t\tprimePlayerWithFirstRow();""",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
APP_JS_PATCHES = [
|
||||
# ---- what the run just rewrote is on the transport too --------------
|
||||
#
|
||||
# The run re-reads every file it touches, so the rows are current — but
|
||||
# the transport and the mixer drew their names when the file was loaded
|
||||
# and nothing tells them to look again. Rename the boom across a card
|
||||
# and the table says Boom Left while the mixer, showing the very file
|
||||
# that was rewritten, still says Boom.
|
||||
(
|
||||
"""\t\t\t\t\tsetStatus( format( t( "bulkEditDone" ), succeeded, failed ) );
|
||||
\t\t\t\t\trenderTableBody();""",
|
||||
"""\t\t\t\t\tsetStatus( format( t( "bulkEditDone" ), succeeded, failed ) );
|
||||
\t\t\t\t\trenderTableBody();
|
||||
\t\t\t\t\tvar onAir = state.playerRow && state.playerRow();
|
||||
\t\t\t\t\tif ( onAir ) {
|
||||
\t\t\t\t\t\tplayerFilename.textContent = onAir.parsed.fileName;
|
||||
\t\t\t\t\t\trenderChannelChips( onAir );
|
||||
\t\t\t\t\t}
|
||||
\t\t\t\t\trenderMixer();""",
|
||||
),
|
||||
# ---- a failed file has to say why -----------------------------------
|
||||
#
|
||||
# This swallowed the reason entirely, and a bulk run that writes nothing
|
||||
@@ -3031,6 +3551,8 @@ app_js = apply_patches(app_js, TRANSPORT_KEY_PATCHES, "transport key patch")
|
||||
app_js = apply_patches(app_js, APP_JS_PATCHES, "app js patch")
|
||||
app_js = apply_patches(app_js, PLAYER_PATCHES, "player patch")
|
||||
app_js = apply_patches(app_js, SPECTRO_PATCHES, "spectrogram patch")
|
||||
app_js = apply_patches(app_js, COLUMN_ORDER_PATCHES, "column order patch")
|
||||
app_js = apply_patches(app_js, FIRST_FILE_PATCHES, "first file patch")
|
||||
shim_js = guard(read(BUILD / "tauri-bridge.js"), "shim js", "script")
|
||||
|
||||
replacements = [
|
||||
|
||||
Reference in New Issue
Block a user