Changeset 37588
- Timestamp:
- 2023-03-28T16:17:01+13:00 (13 months ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js
r37287 r37588 293 293 var wavesurfer; 294 294 295 /** 296 * @param audio input audio file 297 * @param sectionData diarization data (.csv) 298 */ 295 299 function loadAudio(audio, sectionData) { 300 const inputFile = sectionData; 301 const mod_meta_base_url = gs.xsltParams.library_name + "?a=g&rt=r&ro=0&s=ModifyMetadata&s1.collection=" + gs.cgiParams.c + "&s1.site=" + gs.xsltParams.site_name + "&s1.d=" + gs.cgiParams.d; 302 const interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; // path to toolbar images 303 296 304 let editMode = false; 297 305 let currentRegion = {speaker: '', start: '', end: ''}; 298 306 let currentRegions = []; 299 307 300 // let speakerObjects = [];301 // let tempSpeakerObjects = [];302 // let uniqueSpeakers;303 const inputFile = sectionData;304 308 let itemType; 305 309 306 310 let dualMode = false; 307 311 let secondaryLoaded = false; 312 let selectedVersions = ['current']; 308 313 309 314 let waveformCursorX = 0; … … 322 327 let isZooming; 323 328 329 let canvasImages = {}; // stores canvas images of each version for fast loading from cache 330 324 331 let accentColour = "#66d640"; 325 332 // let accentColour = "#F8C537"; … … 328 335 let regionColourSet = []; 329 336 337 330 338 let waveformContainer = document.getElementById("waveform"); 339 let waveformSpinner = document.getElementById('waveform-blocker'); 340 let loader = document.getElementById('waveform-loader'); 341 let initialLoad = true; 331 342 332 343 wavesurfer = WaveSurfer.create({ // wavesurfer options 344 autoCenterImmediately: true, 333 345 container: waveformContainer, 334 backend: " MediaElement",335 backgroundColor: "rgb(40, 54, 58)",336 // backgroundColor: "rgb(24, 36, 39)",346 backend: "WebAudio", 347 // backgroundColor: "rgb(40, 54, 58)", 348 backgroundColor: "rgb(29, 43, 47)", 337 349 waveColor: "white", 338 350 progressColor: accentColour, 339 351 // progressColor: "grey", 340 // barWidth: 2,341 barHeight: 1.2,352 // barWidth: 1, 353 // barHeight: 1.2, 342 354 // barGap: 2, 343 355 // barRadius: 1, 356 height: 140, 344 357 cursorColor: 'black', 345 cursorWidth: 2, 346 normalize: true, // normalizes by maximum peak 358 // maxCanvasWidth: 32000, 359 minPxPerSec: 15, // default 20 360 partialRender: true, // use the PeakCache to improve rendering speed of large waveforms 361 pixelRatio: 1, // 1 results in faster rendering 362 scrollParent: true, 347 363 plugins: [ 348 364 WaveSurfer.regions.create({ … … 356 372 secondaryFontColor: "white", 357 373 notchPercentHeight: "0", 358 fontSize: "12" 374 fontSize: "12", 375 // zoomDebounce: 30, 376 fontFamily: "Courier New" 359 377 }), 360 378 WaveSurfer.cursor.create({ … … 372 390 }); 373 391 374 wavesurfer.load(audio);375 376 // wavesurfer events377 378 wavesurfer.on('region-click', handleRegionClick);379 380 function handleRegionClick(region, e) {381 contextMenu.classList.remove('visible');382 e.stopPropagation();383 if (!editMode) { // play region audio on click384 wavesurfer.play(region.start); // plays from start of region385 } else { // select or deselect current region386 if (region.element.classList.contains("region-top")) {387 currSpeakerSet = primarySet;388 swapCarets(true);389 } else if (region.element.classList.contains("region-bottom")) {390 currSpeakerSet = secondarySet;391 swapCarets(false);392 }393 prevUndoState = "";394 395 if (!e.ctrlKey && !e.shiftKey) {396 currentRegions = [];397 if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) {398 // removeCurrentRegion(); // deselect current region on click399 } else {400 currentRegion = region;401 currentRegion.speaker = currentRegion.attributes.label.innerText;402 region.play(); // start and stop to move play cursor to beginning of region403 wavesurfer.playPause();404 }405 } else if (e.ctrlKey) { // control was held during click406 if (currentRegions.length == 0 && isCurrentRegion(region)) {407 removeCurrentRegion();408 } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) {409 const removeIndex = getIndexInCurrentRegions(region);410 if (removeIndex != -1) currentRegions.splice(removeIndex, 1);411 if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed412 currentRegion = currentRegions[0];413 }414 } else {415 if (currentRegions.length < 1) currentRegions.push(currentRegion);416 if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist417 currentRegion = region;418 currentRegion.speaker = currentRegion.attributes.label.innerText;419 region.play();420 wavesurfer.playPause();421 }422 if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one423 } else if (e.shiftKey) { // shift was held during click424 clearChapterSearch();425 if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {426 if (currentRegions && currentRegions.length > 0) {427 if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down428 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1);429 } else { // shifting backwards / up430 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1);431 }432 } else {433 if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down434 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1);435 } else { // shifting backwards / up436 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1);437 }438 }439 }440 }441 if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }442 reloadRegionsAndChapters();443 }444 }445 446 function getIndexInCurrentRegions(region) {447 for (const reg of currentRegions) {448 const regSpeaker = reg.attributes ? reg.attributes.label.innerText : reg.speaker;449 if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label.innerText) {450 return currentRegions.indexOf(reg);451 }452 }453 return -1;454 }455 456 function getIndexOfRegion(region) {457 for (const reg of currSpeakerSet.tempSpeakerObjects) {458 if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label.innerText) {459 return currSpeakerSet.tempSpeakerObjects.indexOf(reg);460 }461 }462 return -1;463 }464 465 wavesurfer.on('region-mouseenter', function(region) { // region hover effects466 if (!mouseDown) {467 handleRegionColours(region, true);468 setHoverSpeaker(region.element.style.left, region.attributes.label.innerText);469 if (!isInCurrentRegions(region)) {470 removeRegionBounds();471 drawRegionBounds(region, wave.scrollLeft, "black");472 }473 if (isCurrentRegion(region) && editMode) drawRegionBounds(region, wave.scrollLeft, "FireBrick");474 }475 });476 477 function setHoverSpeaker(offset, name) {478 hoverSpeaker.innerHTML = name;479 let newOffset = parseInt(offset.slice(0, -2)) - wave.scrollLeft;480 // if (newOffset < 0) newOffset = 0;481 hoverSpeaker.style.marginLeft = newOffset + "px";482 }483 484 wavesurfer.on('region-mouseleave', function(region) {485 hoverSpeaker.innerHTML = "";486 if (!mouseDown) {487 if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false);488 if (!editMode) hoverSpeaker.innerHTML = "";489 removeRegionBounds();490 if (currentRegion.speaker && getCurrentRegionIndex() != -1) {491 setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);492 drawCurrentRegionBounds();493 }494 }495 });496 wavesurfer.on('region-in', function(region) {497 // handleRegionColours(region, true);498 if (itemType == "chapter" && Array.from(chapters.children)[getIndexOfRegion(region)]) {499 Array.from(chapters.children)[getIndexOfRegion(region)].scrollIntoView({500 behavior: "smooth",501 block: "nearest"502 });503 }504 });505 wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });506 wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event507 wavesurfer.on('region-updated', handleRegionSnap);508 509 let loader = document.createElement("span"); // loading audio element510 loader.innerHTML = "Loading audio";511 loader.id = "waveform-loader";512 document.querySelector("#waveform wave").prepend(loader);513 514 wavesurfer.on('waveform-ready', function() { // retrieve regions once waveforms have loaded515 if (inputFile.endsWith("csv")) { // diarization if csv516 itemType = "chapter";517 if (localStorage.getItem('undoStates') && localStorage.getItem('undoLevel')) {518 console.log('-- Loading regions from localStorage --');519 undoStates = JSON.parse(localStorage.getItem('undoStates'));520 undoLevel = JSON.parse(localStorage.getItem('undoLevel'));521 primarySet.tempSpeakerObjects = undoStates[undoLevel].state;522 primarySet.uniqueSpeakers = [];523 for (const item of primarySet.tempSpeakerObjects) {524 if (!primarySet.uniqueSpeakers.includes(item.speaker)) primarySet.uniqueSpeakers.push(item.speaker);525 }526 populateChapters(primarySet);527 if (undoStates[undoLevel].secState && undoStates[undoLevel].secState.length > 0) {528 secondarySet.tempSpeakerObjects = undoStates[undoLevel].secState;529 secondarySet.uniqueSpeakers = [];530 for (const item of secondarySet.tempSpeakerObjects) {531 if (!secondarySet.uniqueSpeakers.includes(item.speaker)) secondarySet.uniqueSpeakers.push(item.speaker);532 }533 secondaryLoaded = true;534 // editButton.click(); // open edit panel and enable dual mode if secondary set was previously altered535 // dualModeCheckbox.checked = true;536 // dualModeChanged(true);537 }538 updateRegionEditPanel();539 } else {540 loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);541 dualModeCheckbox.checked = true;542 dualModeChanged(true);543 544 setTimeout(()=>{545 dualModeCheckbox.checked = false;546 dualModeChanged(true);547 }, 150)548 }549 } else if (inputFile.endsWith("json")) { // transcription if json550 itemType = "word";551 loadJSONFile(inputFile);552 } else {553 console.log("Filetype of " + inputFile + " not supported.")554 }555 556 loader.remove(); // remove load text557 chapters.style.cursor = "default"; // remove load cursor558 wave.className = "audio-scroll";559 drawVersionNames(); // draw version names if editPanel is expanded560 });561 562 function downloadURI(loc, name) {563 let link = document.createElement("a");564 link.download = name;565 link.href = loc;566 link.click();567 }568 569 392 // toolbar elements & event handlers 570 393 const audioContainer = document.getElementById("audioContainer"); 571 394 const dualModeCheckbox = document.getElementById("dual-mode-checkbox"); 572 395 const wave = document.getElementsByTagName("wave")[0]; 396 const caretContainer = document.getElementById("caret-container"); 573 397 const primaryCaret = document.getElementById("primary-caret"); 574 398 const secondaryCaret = document.getElementById("secondary-caret"); … … 602 426 const hoverSpeaker = document.getElementById("hover-speaker"); 603 427 const contextMenu = document.getElementById("context-menu"); 604 const contextDelete = document.getElementById("context-menu-delete");605 428 const contextReplace = document.getElementById("context-menu-replace"); 606 429 const contextOverdub = document.getElementById("context-menu-overdub"); 607 // const contextCopy = document.getElementById("context-menu-copy"); 608 const contextSave = document.getElementById("context-menu-save"); 609 const dualModeMenuButton = document.getElementById("dual-mode-menu-button"); 610 const dualModeMenu = document.getElementById("dual-mode-menu"); 430 const contextLock = document.getElementById("context-menu-lock"); 431 const contextDelete = document.getElementById("context-menu-delete"); 432 const timelineMenu = document.getElementById("timeline-menu"); 433 const timelineMenuButton = document.getElementById("timeline-menu-button"); 434 const timelineMenuHide = document.getElementById("timeline-menu-hide"); 435 const timelineMenuDualMode = document.getElementById("timeline-menu-dualmode"); 436 const timelineMenuRegionConflict = document.getElementById("timeline-menu-region"); 437 const timelineMenuSpeakerConflict = document.getElementById("timeline-menu-speaker"); 438 const versionSelectMenu = document.getElementById('version-select-menu'); 439 const versionSelectLabels = document.querySelectorAll(".track-arrow"); 440 const savePopup = document.getElementById("save-popup"); 441 const savePopupBG = document.getElementById("save-popup-bg"); 442 const savePopupCancel = document.getElementById("save-popup-cancel"); 443 const savePopupCommit = document.getElementById("save-popup-commit"); 444 const savePopupCommitMsg = document.getElementById("commit-message"); 611 445 612 446 audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() }); … … 631 465 editButton.addEventListener("click", toggleEditMode); 632 466 downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) }); 633 muteButton.addEventListener("click", () => { wavesurfer.toggleMute() }); 467 muteButton.addEventListener("click", () => { 468 if (volumeSlider.value == 0) wavesurfer.setMute(false) 469 else wavesurfer.setMute(true) 470 }); 634 471 volumeSlider.style["accent-color"] = accentColour; 635 472 fullscreenButton.addEventListener("click", toggleFullscreen); … … 640 477 createButton.addEventListener("click", createNewRegion); 641 478 removeButton.addEventListener("click", removeRegion); 642 discardButton.addEventListener("click", discardRegionChanges);479 discardButton.addEventListener("click", () => discardRegionChanges(false)); 643 480 undoButton.addEventListener("click", undo); 644 481 redoButton.addEventListener("click", redo); 645 482 saveButton.addEventListener("click", saveRegionChanges); 646 document.addEventListener('click', () => contextMenu.classList.remove('visible'));483 document.addEventListener('click', documentClicked); 647 484 document.addEventListener('mouseup', () => mouseDown = false); 648 485 document.addEventListener('mousedown', (e) => { if (e.target.id !== "create-button") newRegionOffset = 0 }); // resets new region offset on click … … 651 488 e.onblur = () => { prevUndoState = "" }; 652 489 }); 653 contextDelete.addEventListener("click", removeRightClicked);654 490 contextReplace.addEventListener("click", replaceSelected); 655 491 contextOverdub.addEventListener("click", overdubSelected); 656 // contextCopy.addEventListener("click", copySelected); 657 contextSave.addEventListener("click", saveSelected); 658 dualModeMenuButton.addEventListener("click", dualModeMenuToggle); 659 dualModeMenuButton.addEventListener("click", dualModeMenuToggle); 660 492 contextLock.addEventListener("click", toggleLockSelected); 493 contextDelete.addEventListener("click", removeRightClicked); 494 timelineMenu.addEventListener("click", e => e.stopPropagation()); 495 timelineMenuButton.addEventListener("click", timelineMenuToggle); 496 timelineMenuHide.addEventListener("click", timelineMenuHideClicked); 497 timelineMenuDualMode.addEventListener("click", () => { dualModeChanged() }); 498 timelineMenuRegionConflict.addEventListener("click", showStartStopConflicts); 499 timelineMenuSpeakerConflict.addEventListener("click", showSpeakerNameConflicts); 500 501 savePopupCancel.addEventListener("click", toggleSavePopup) 502 savePopupCommit.addEventListener("click", commitChanges); 503 savePopupBG.addEventListener("click", toggleSavePopup); 504 versionSelectLabels.forEach(arrow => arrow.addEventListener('click', toggleVersionDropdown)); 505 506 volumeSlider.addEventListener("input", function() { 507 wavesurfer.setVolume(this.value); 508 if (this.value == 0) { 509 muteButton.src = interface_bootstrap_images + "mute.svg"; 510 muteButton.style.opacity = 0.6; 511 } else { 512 muteButton.src = interface_bootstrap_images + "unmute.svg"; 513 muteButton.style.opacity = 1; 514 } 515 }); 516 517 zoomSlider.addEventListener('input', function() { // slider changes waveform zoom 518 wavesurfer.zoom(Number(this.value) / 4); 519 if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 520 setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); 521 drawCurrentRegionBounds(); 522 } 523 let handles = document.getElementsByClassName("wavesurfer-handle"); 524 if (this.value < 20) { 525 for (const handle of handles) handle.style.setProperty("width", "1px", "important"); 526 } else { 527 for (const handle of handles) handle.style.setProperty("width", "3px", "important"); 528 } 529 }); 530 showAudioLoader(); 531 661 532 if (gs.variables.allowEditing === '0') { editButton.style.display = "none" } 662 533 663 function chapterSearchInputChange(e) { 534 wavesurfer.load(audio); 535 536 // wavesurfer events 537 538 wavesurfer.on('region-click', handleRegionClick); 539 wavesurfer.on('region-mouseenter', function(region) { // region hover effects 540 if (!mouseDown) { 541 handleRegionColours(region, true); 542 setHoverSpeaker(region.element.style.left, region.attributes.label.innerText); 543 if (!isInCurrentRegions(region)) { 544 removeRegionBounds(); 545 drawRegionBounds(region, wave.scrollLeft, "black"); 546 } 547 if (isCurrentRegion(region) && editMode) drawRegionBounds(region, wave.scrollLeft, "FireBrick"); 548 } 549 }); 550 wavesurfer.on('region-mouseleave', function(region) { 551 hoverSpeaker.innerHTML = ""; 552 if (!mouseDown) { 553 if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); 554 if (!editMode) hoverSpeaker.innerHTML = ""; 555 removeRegionBounds(); 556 if (currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1) { 557 setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); 558 drawCurrentRegionBounds(); 559 } 560 } 561 }); 562 wavesurfer.on('region-in', function(region) { // play caret enters region 563 if (!mouseDown) { 564 handleRegionColours(region, true); 565 if (itemType == "chapter" && Array.from(chapters.children)[getIndexOfRegion(region)]) { 566 Array.from(chapters.children)[getIndexOfRegion(region)].scrollIntoView({ 567 behavior: "smooth", 568 block: "nearest" 569 }); 570 } 571 } 572 }); 573 wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) }); 574 wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event 575 wavesurfer.on('region-updated', handleRegionSnap); 576 wavesurfer.on('error', error => console.log(error)); 577 578 wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; }); 579 wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; }); 580 wavesurfer.on("mute", function(mute) { 581 if (mute) { 582 muteButton.src = interface_bootstrap_images + "mute.svg"; 583 muteButton.style.opacity = 0.6; 584 volumeSlider.value = 0; 585 } 586 else { 587 muteButton.src = interface_bootstrap_images + "unmute.svg"; 588 muteButton.style.opacity = 1; 589 volumeSlider.value = 1; 590 } 591 }); 592 593 wavesurfer.on('ready', function() { // retrieve regions once waveforms have loaded 594 window.onbeforeunload = (e) => { 595 if (undoStates.length > 1) { 596 console.log('undoStates.length: ' + undoStates.length); 597 e.returnValue = "Data will be lost if you leave the page, are you sure?"; 598 return "Data will be lost if you leave the page, are you sure?"; 599 } 600 }; 601 if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove(); 602 setTimeout(() => { // if not delayed exportImage does not retrieve waveform (despite being in waveform-ready?) 603 const currVersion = selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1]; 604 for (let key in canvasImages) { 605 if (currVersion == key && canvasImages[key] == undefined) { canvasImages[key] = wavesurfer.exportImage() } // add waveform image to cache if one isn't already assigned to the version 606 } 607 }, 1000); 608 609 if (initialLoad) { 610 if (inputFile.endsWith("csv")) { // diarization if csv 611 itemType = "chapter"; 612 if (localStorage.getItem('undoStates') && localStorage.getItem('undoLevel')) { 613 console.log('-- Loading regions from localStorage --'); 614 undoStates = JSON.parse(localStorage.getItem('undoStates')); 615 undoLevel = JSON.parse(localStorage.getItem('undoLevel')); 616 primarySet.tempSpeakerObjects = undoStates[undoLevel].state; 617 primarySet.speakerObjects = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); 618 primarySet.uniqueSpeakers = []; 619 for (const item of primarySet.tempSpeakerObjects) { 620 if (!primarySet.uniqueSpeakers.includes(item.speaker)) primarySet.uniqueSpeakers.push(item.speaker); 621 } 622 populateChaptersAndRegions(primarySet); 623 if (undoStates[undoLevel].secState && undoStates[undoLevel].secState.length > 0) { 624 secondarySet.tempSpeakerObjects = undoStates[undoLevel].secState; 625 secondarySet.speakerObjects = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects); 626 secondarySet.uniqueSpeakers = []; 627 for (const item of secondarySet.tempSpeakerObjects) { 628 if (!secondarySet.uniqueSpeakers.includes(item.speaker)) secondarySet.uniqueSpeakers.push(item.speaker); 629 } 630 secondaryLoaded = true; 631 } 632 updateRegionEditPanel(); 633 } else { 634 loadCSVFile(inputFile, primarySet); 635 dualModeChanged(true, "true"); 636 setTimeout(()=>{ 637 dualModeChanged(true, "false"); 638 }, 150) 639 } 640 } else if (inputFile.endsWith("json")) { // transcription if json 641 itemType = "word"; 642 loadJSONFile(inputFile); 643 } else { 644 console.log("Filetype of " + inputFile + " not supported.") 645 } 646 647 chapters.style.cursor = "default"; // remove load cursor 648 wave.className = "audio-scroll"; 649 $.ajax({ 650 type: "GET", 651 url: gs.variables.metadataServerURL, 652 data: { a: 'get-fldv-info', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d }, 653 dataType: "json", 654 }).then(data => { 655 for (const version of ["current", ...data]) { 656 canvasImages[version] = undefined; 657 let menuItem = document.createElement("div"); 658 menuItem.classList.add("version-select-menu-item"); 659 menuItem.id = version; 660 let text = version.includes("nminus") ? version.replace("nminus-", "Previous(") + ")" : version; 661 menuItem.innerText = text.charAt(0).toUpperCase() + text.slice(1); 662 menuItem.addEventListener('click', versionClicked); 663 let dataObj = { a: 'get-archives-metadata', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d, metaname: "commitmessage" }; 664 if (version != "current") Object.assign(dataObj, {dv: version}); 665 $.ajax({ // get commitmessage metadata to show as hover tooltip 666 type: "GET", 667 url: gs.variables.metadataServerURL, 668 data: dataObj, 669 dataType: "text", 670 }).then(comment => { 671 menuItem.title = "Commit message: " + comment; 672 versionSelectMenu.append(menuItem); 673 [...versionSelectMenu.children].sort((a,b) => a.innerText>b.innerText?1:-1).forEach(n=>versionSelectMenu.appendChild(n)); // sort alphabetically 674 }, (error) => { console.log("get-archives-metadata error:"); console.log(error); }); 675 } 676 }, (error) => { console.log("get-fldv-info error:"); console.log(error); }); 677 initialLoad = false; 678 } 679 // fixes blank waveform/regions when loading Current -> Prev.1 -> Prev.2 680 zoomSlider.value = 25; 681 zoomSlider.dispatchEvent(new Event("input")); 682 wavesurfer.zoom(50 / 4); 683 hideAudioLoader(); 684 }); 685 686 /** 687 * Draws string above waveform at the provided offset 688 * @param {number} offset Offset (from left) to desired location 689 * @param {string} name String to be drawn 690 */ 691 function setHoverSpeaker(offset, name) { 692 hoverSpeaker.innerHTML = name; 693 let newOffset = parseInt(offset.slice(0, -2)) - wave.scrollLeft; 694 hoverSpeaker.style.marginLeft = newOffset + "px"; 695 } 696 697 /** Click handler, manages selected region/s, set swapping, region playing */ 698 function handleRegionClick(region, e) { 699 if (e.target.classList.contains("region-menu")) return; 700 e.stopPropagation(); 701 contextMenu.classList.remove('visible'); 702 if (!editMode) { // play region audio on click 703 wavesurfer.play(region.start); // plays from start of region 704 } else { // select or deselect current region 705 if (!region.element) return; 706 if (region.element.classList.contains("region-top")) { 707 currSpeakerSet = primarySet; 708 swapCarets(true); 709 } else if (region.element.classList.contains("region-bottom")) { 710 currSpeakerSet = secondarySet; 711 swapCarets(false); 712 } 713 prevUndoState = ""; 714 715 if (!e.ctrlKey && !e.shiftKey) { 716 currentRegions = []; 717 currentRegion = region; 718 currentRegion.speaker = currentRegion.attributes.label.innerText; 719 wavesurfer.backend.seekTo(currentRegion.start); 720 } else if (e.ctrlKey) { // control was held during click 721 if (currentRegions.length == 0 && isCurrentRegion(region)) { 722 removeCurrentRegion(); 723 } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) { 724 const removeIndex = getIndexInCurrentRegions(region); 725 if (removeIndex != -1) currentRegions.splice(removeIndex, 1); 726 if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed 727 currentRegion = currentRegions[0]; 728 } 729 } else { 730 if (currentRegions.length < 1) currentRegions.push(currentRegion); 731 if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist 732 currentRegion = region; 733 currentRegion.speaker = currentRegion.attributes.label.innerText; 734 wavesurfer.backend.seekTo(currentRegion.start); 735 } 736 if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one 737 } else if (e.shiftKey) { // shift was held during click 738 clearChapterSearch(); 739 if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) { 740 if (currentRegions && currentRegions.length > 0) { 741 if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down 742 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1); 743 } else { // shifting backwards / up 744 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1); 745 } 746 } else { 747 if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down 748 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1); 749 } else { // shifting backwards / up 750 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1); 751 } 752 } 753 } 754 } 755 if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) } 756 reloadRegionsAndChapters(); 757 } 758 } 759 760 /** 761 * Returns index of given region within the currently selected regions 762 * @param {object} region Region within currently selected regions to return index for 763 * @returns {int} Index position of region 764 */ 765 function getIndexInCurrentRegions(region) { 766 for (const reg of currentRegions) { 767 const regSpeaker = reg.attributes ? reg.attributes.label.innerText : reg.speaker; 768 if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label.innerText) { 769 return currentRegions.indexOf(reg); 770 } 771 } 772 return -1; 773 } 774 775 /** 776 * Returns index of region within speakerObject array 777 * @param {object} region Region to return index for 778 * @returns {int} Index position of region 779 */ 780 function getIndexOfRegion(region) { 781 for (const reg of currSpeakerSet.tempSpeakerObjects) { 782 if (region.attributes && reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label.innerText) { 783 return currSpeakerSet.tempSpeakerObjects.indexOf(reg); 784 } 785 } 786 return -1; 787 } 788 789 /** 790 * Builds metadata-server.pl URL to retrieve audio at given version 791 * @param {string} version GS document version to retrieve from (nminus-X) 792 */ 793 function getAudioURLFromVersion(version) { 794 let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d; 795 if (version !== "current") base_url += "&dv=" + version // get fldv if not current version 796 return base_url + "&assocname=" + gs.documentMetadata.Audio; 797 } 798 799 /** 800 * Builds metadata-server.pl URL to retrieve CSV at given version 801 * @param {string} version GS document version to retrieve from (nminus-X) 802 */ 803 function getCSVURLFromVersion(version) { 804 let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d; 805 if (version !== "current") base_url += "&dv=" + version; // get fldv if not current version 806 return base_url + "&assocname=" + "structured-audio.csv"; 807 } 808 809 /** Version click handler, first checks if changes have been made and shows popup if true */ 810 function versionClicked(e) { 811 let unsavedChanges = false; 812 if (undoStates.length > 0) { // only if changes have been made in track being changed FROM 813 let clickedVersionPos = e.target.parentElement.classList.contains('versionTop') ? 0 : 1; 814 for (const state of undoStates) { 815 if (state.changedTrack == selectedVersions[clickedVersionPos]) { 816 unsavedChanges = true; 817 break; 818 } 819 } 820 } 821 if (unsavedChanges) { 822 const areYouSure = "There are unsaved changes.\nAre you sure you want to lose changes made in this version?"; 823 if (window.confirm(areYouSure)) { 824 console.log('OK'); 825 discardRegionChanges(true); 826 changeVersion(e); 827 } else { 828 console.log('CANCEL'); 829 return; 830 } 831 } else changeVersion(e); 832 } 833 834 /** Changes current audio/csv set to clicked version's equivalent */ 835 function changeVersion(e) { 836 removeCurrentRegion(); 837 const audio_url = getAudioURLFromVersion(e.target.id); 838 const csv_url = getCSVURLFromVersion(e.target.id); 839 versionSelectMenu.classList.remove('visible'); 840 const setToUpdate = e.target.parentElement.classList.contains('versionTop') ? primarySet : secondarySet; 841 if (e.target.parentElement.classList.contains('versionTop')) { 842 if (!currSpeakerSet.isSecondary) { 843 if (dualMode) $(".region-top").remove(); 844 else $(".wavesurfer-region").remove(); 845 showAudioLoader(); 846 // if (canvasImages[e.target.id]) { // if waveform image exists in cache 847 // drawImageOnWaveform(canvasImages[e.target.id]); 848 // } 849 wavesurfer.load(audio_url); // load audio 850 } else { 851 $(".region-top").remove(); 852 } 853 document.getElementById('track-set-label-top').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update top label text 854 selectedVersions[0] = e.target.id; // update the selected versions 855 } else { 856 if (currSpeakerSet.isSecondary) { 857 if (dualMode) $(".region-bottom").remove(); 858 else $(".wavesurfer-region").remove(); 859 showAudioLoader(); 860 // if (canvasImages[e.target.id]) { // if waveform image exists in cache 861 // drawImageOnWaveform(canvasImages[e.target.id]); 862 // } 863 wavesurfer.load(audio_url); 864 } else { 865 $(".region-bottom").remove(); 866 } 867 document.getElementById('track-set-label-bottom').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update bottom label text 868 selectedVersions[1] = e.target.id; 869 } 870 loadCSVFile(csv_url, setToUpdate, true); 871 } 872 873 /** Utility function to download audio */ 874 function downloadURI(loc, name) { 875 let link = document.createElement("a"); 876 link.download = name; 877 link.href = loc; 878 link.click(); 879 } 880 881 /** Document click listener for context box closure and region deselection */ 882 function documentClicked(e) { // document on click 883 if (e.target.classList.contains("region-menu")) return; 884 contextMenu.classList.remove('visible'); 885 timelineMenu.classList.remove('visible'); 886 versionSelectMenu.classList.remove('visible'); 887 versionSelectLabels.forEach(arrow => { 888 // arrow.style.transform = 'rotate(90deg)'; 889 // arrow.style.paddingTop = '0'; 890 arrow.style.display = 'inline'; 891 }); 892 if (editMode && e.target.tagName !== "INPUT" && e.target.tagName !== "IMG" && !e.target.classList.contains("ui-button") && !$("#audio-dropdowns").has($(e.target)).length 893 && !e.target.classList.contains("context-menu-item")) { 894 let currReg = getCurrentRegionIndex() != -1 ? currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region : false; // save for deselection 895 let currRegs = getCurrentRegionsIndexes().length > 1 ? currentRegions : false; // save for deselection 896 removeCurrentRegion(); 897 reloadChapterList(); 898 if (currReg != false) regionLeave(currReg); // deselect curr region 899 if (currRegs != false) { 900 for (const reg of currRegs) { 901 regionLeave(reg.region); // deselect curr regions 902 regionLeave(reg.region); // deselect curr regions 903 } 904 } 905 removeRegionBounds(); 906 removeButton.innerHTML = "Remove Selected Region"; 907 updateRegionEditPanel(); 908 } 909 } 910 911 /** Draws and returns padlock image at given parent element */ 912 function drawPadlock(parent) { 913 let lockedImg = document.createElement("img"); 914 lockedImg.classList.add("region-padlock"); 915 lockedImg.src = interface_bootstrap_images + "lock.svg"; 916 lockedImg.title = "This region is locked. Click to unlock region."; 917 parent.prepend(lockedImg); 918 return lockedImg; 919 } 920 921 /** 922 * Draws triple dot menu button and attaches click listener 923 * @param {object} region Region to attach menu button to 924 */ 925 function drawMenuButton(region) { 926 let menuImg = document.createElement("img"); 927 menuImg.src = interface_bootstrap_images + "menu.svg"; 928 menuImg.classList.add("region-menu"); 929 menuImg.title = "Show region options"; 930 menuImg.addEventListener("click", e => { 931 audioContainer.dispatchEvent(new MouseEvent("contextmenu", { clientX: menuImg.x + 20, clientY: menuImg.y + 5 })); 932 }); 933 region.element.append(menuImg); 934 } 935 936 /** 937 * Attaches a click listener to given padlock element 938 * @param padlock Element to attach listener to 939 * @param region Associated region 940 * @param isChapter Whether padlock exists in chapter (true) or wavesurfer region (false) 941 */ 942 function attachPadlockListener(padlock, region, isChapter) { 943 if (isChapter == true) { 944 padlock.addEventListener('click', () => { // attach to chapter padlock 945 let index = getIndexOfRegion(region); 946 currSpeakerSet.tempSpeakerObjects[index].locked = false; 947 padlock.classList.add('hide'); 948 if (currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild) currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild.remove(); 949 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index); 950 }); 951 } else { 952 padlock.addEventListener('click', () => { // attach to region padlock 953 let index = getIndexOfRegion(region); 954 currSpeakerSet.tempSpeakerObjects[index].locked = false; 955 padlock.remove(); 956 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index); 957 }); 958 } 959 } 960 961 /** Locks or unlocks selected region based on its current state */ 962 function toggleLockSelected(e) { // locks / unlocks selected region(s) 963 if (e) e.stopPropagation(); 964 if (getCurrentRegionIndex() != -1 && currentRegions.length <= 1) { // single selected 965 let currIndex = getCurrentRegionIndex(); 966 currSpeakerSet.tempSpeakerObjects[currIndex].locked = !e.target.innerText.includes("Unlock"); 967 if (currSpeakerSet.tempSpeakerObjects[currIndex].locked) { 968 chapters.childNodes[currIndex].childNodes[1].classList.remove('hide'); 969 let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[currIndex].region.element); 970 attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[currIndex].region, false); 971 contextLock.innerText = "Unlock Selected"; 972 } else { 973 chapters.childNodes[currIndex].childNodes[1].classList.add('hide'); 974 if (currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock").length > 0) { 975 currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock")[0].remove(); 976 } 977 contextLock.innerText = "Lock Selected"; 978 } 979 } else if (currentRegions.length > 1) { // multiple selected 980 let toLock = !e.target.innerText.includes("Unlock"); 981 for (const idx of getCurrentRegionsIndexes()) { 982 currSpeakerSet.tempSpeakerObjects[idx].locked = toLock; 983 if (currSpeakerSet.tempSpeakerObjects[idx].locked) { 984 chapters.childNodes[idx].childNodes[1].classList.remove('hide'); 985 if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length == 0) { 986 let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[idx].region.element); 987 attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[idx].region, false); 988 } 989 contextLock.innerText = "Unlock Selected"; 990 } else { 991 chapters.childNodes[idx].childNodes[1].classList.add('hide'); 992 if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length > 0) { 993 currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock")[0].remove(); 994 } 995 contextLock.innerText = "Lock Selected"; 996 } 997 } 998 if (document.getElementById("context-menu-lock-2")) document.getElementById("context-menu-lock-2").remove(); 999 } 1000 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", getCurrentRegionIndex()); 1001 } 1002 1003 /** TODO */ 1004 function timelineMenuHideClicked(e) { // hides all regions and chapter/edit divs 1005 if (!e.target.children[0].checked) { 1006 e.target.children[0].checked = true; 1007 timelineMenuDualMode.classList.add('disabled'); 1008 timelineMenuRegionConflict.classList.add('disabled'); 1009 timelineMenuSpeakerConflict.classList.add('disabled'); 1010 if (editPanel.style.height != "0px") toggleEditMode(); 1011 if (chapters.style.height != "0px") toggleChapters(); 1012 $('.wavesurfer-region').fadeOut(100); 1013 } 1014 else { 1015 e.target.children[0].checked = false; 1016 timelineMenuDualMode.classList.remove('disabled'); 1017 timelineMenuRegionConflict.classList.remove('disabled'); 1018 timelineMenuSpeakerConflict.classList.remove('disabled'); 1019 let fadeIn = true; 1020 if (timelineMenuRegionConflict.firstElementChild.checked) { 1021 showStartStopConflicts(e, true); 1022 fadeIn = false; 1023 } 1024 if (timelineMenuSpeakerConflict.firstElementChild.checked) { 1025 showSpeakerNameConflicts(e, true); 1026 fadeIn = false; 1027 } 1028 if (fadeIn) $('.wavesurfer-region').fadeIn(100); 1029 } 1030 } 1031 1032 function chapterSearchInputChange(e) { // filters chapters and regions by given speaker name 664 1033 if (e.isTrusted) { // triggered from user action 665 1034 if (document.getElementById("chapter-alert")) document.getElementById("chapter-alert").remove(); … … 695 1064 } 696 1065 697 function clearChapterSearch() { 1066 function clearChapterSearch() { // clears search filter and updates results 698 1067 chapterSearchInput.value = ""; 699 1068 chapterSearchInput.dispatchEvent(new Event("input")); 700 1069 } 701 1070 702 function dualModeMenuToggle() { 703 if (editMode && dualMode) { 704 if (dualModeMenu.classList.contains('visible')) dualModeMenu.classList.remove('visible'); 705 else dualModeMenu.classList.add('visible'); 706 } 707 } 708 709 function handleRegionSnap(region, e) { 710 if (editMode && currentRegion) { 1071 function showStartStopConflicts(e, forceRun) { // hides regions that have identical start/stop time 1072 removeCurrentRegion(); 1073 if ((dualMode && !timelineMenuRegionConflict.children[0].checked) || forceRun) { 1074 timelineMenuRegionConflict.children[0].checked = true; 1075 let primHide = []; 1076 let secHide = []; 1077 if (!timelineMenuSpeakerConflict.children[0].checked) hideAll(); 1078 for (const primIdx in primarySet.tempSpeakerObjects) { 1079 for (const secIdx in secondarySet.tempSpeakerObjects) { 1080 if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx])) { // if regions have same start/end time, hide 1081 primHide.push(primIdx); 1082 secHide.push(secIdx); 1083 } 1084 } 1085 } 1086 for (const primIdx in primarySet.tempSpeakerObjects) { 1087 if (!primHide.includes(primIdx)) { 1088 primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; 1089 if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex"; 1090 } 1091 } 1092 for (const secIdx in secondarySet.tempSpeakerObjects) { 1093 if (!secHide.includes(secIdx)) { 1094 secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; 1095 if (secondaryCaret.src.includes('fill')) chapters.children[secIdx].style.display = "flex"; 1096 } 1097 } 1098 } else { 1099 timelineMenuRegionConflict.children[0].checked = false; 1100 if (timelineMenuSpeakerConflict.children[0].checked) showSpeakerNameConflicts(e, true); 1101 else clearConflicts(); 1102 } 1103 } 1104 1105 function showSpeakerNameConflicts(e, forceRun) { // shows regions that have identical start/stop time but different names 1106 removeCurrentRegion(); 1107 if ((dualMode && !timelineMenuSpeakerConflict.children[0].checked) || forceRun) { 1108 timelineMenuSpeakerConflict.children[0].checked = true; 1109 if (!timelineMenuRegionConflict.children[0].checked) hideAll(); 1110 for (const primIdx in primarySet.tempSpeakerObjects) { 1111 for (const secIdx in secondarySet.tempSpeakerObjects) { 1112 if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx]) && 1113 primarySet.tempSpeakerObjects[primIdx].speaker != secondarySet.tempSpeakerObjects[secIdx].speaker) { // hide if regions match but names don't 1114 primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; 1115 secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; 1116 if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex"; 1117 else chapters.children[secIdx].style.display = "flex"; 1118 } 1119 } 1120 } 1121 } else { 1122 timelineMenuSpeakerConflict.children[0].checked = false; 1123 if (timelineMenuRegionConflict.children[0].checked) showStartStopConflicts(e, true); 1124 else clearConflicts(); 1125 } 1126 } 1127 1128 function clearConflicts() { // shows all regions and chapters 1129 for (const primIdx in primarySet.tempSpeakerObjects) { 1130 for (const secIdx in secondarySet.tempSpeakerObjects) { 1131 primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; 1132 secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; 1133 chapters.children[primIdx].style.display = "flex"; 1134 } 1135 } 1136 } 1137 1138 function hideAll() { // hides all regions and chapters 1139 for (const primIdx in primarySet.tempSpeakerObjects) { 1140 for (const secIdx in secondarySet.tempSpeakerObjects) { 1141 primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "none"; 1142 secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "none"; 1143 chapters.children[primIdx].style.display = "none"; 1144 } 1145 } 1146 } 1147 1148 function timelineMenuToggle(e) { // shows / hides timeline menu 1149 e.stopPropagation(); 1150 if (timelineMenu.classList.contains('visible')) { 1151 timelineMenu.classList.remove('visible'); 1152 e.target.style.transform = 'rotate(0deg)'; 1153 } 1154 else { 1155 timelineMenu.classList.add('visible'); 1156 e.target.style.transform = 'rotate(-90deg)'; 1157 } 1158 } 1159 1160 function handleRegionSnap(region, e) { // clips region to opposite set region if nearby, called on region update (lots) 1161 if (editMode && currentRegion && !wavesurfer.isPlaying()) { 711 1162 removeRegionBounds(); 712 1163 setHoverSpeaker(region.element.style.left, currentRegion.speaker); 713 drawRegionBounds(region, wave.scrollLeft, "FireBrick"); 1164 drawRegionBounds(region, wave.scrollLeft, "FireBrick"); // gets set to red if currRegion 714 1165 if (e && e.action === "resize" && dualMode && editMode && !ctrlDown) { // won't actuate on drag 715 1166 let oppositeSet = secondarySet; // look down … … 728 1179 } 729 1180 1181 /** 1182 * Returns snap value if near [snapRadius] adjacent region edge 1183 * @param newDragPos Drag position in seconds to check for 1184 * @param speakerSet Adjacent region set 1185 * @returns {number} If found, returns snapped position, otherwise returns input position 1186 */ 730 1187 function getSnapValue(newDragPos, speakerSet) { 731 1188 const snapRadius = 1; 732 1189 for (const region of speakerSet) { // scan opposite region for potential snapping points 733 1190 if (newDragPos > parseFloat(region.start) - snapRadius && newDragPos < parseFloat(region.start) + snapRadius) { 734 // console.log("snap to start: " + region.start);735 1191 snappedTo = "start"; 736 1192 if (snappedToX == 0) snappedToX = waveformCursorX; … … 738 1194 } 739 1195 if (newDragPos > parseFloat(region.end) - snapRadius && newDragPos < parseFloat(region.end) + snapRadius) { 740 // console.log("snap to end: " + region.end);741 1196 snappedTo = "end"; 742 1197 if (snappedToX == 0) snappedToX = waveformCursorX; 743 1198 return region.end; 744 1199 } 745 746 1200 if (snappedTo !== "none" && (waveformCursorX - snappedToX > 10 || waveformCursorX - snappedToX < -10)) { 747 // console.log('released!');748 1201 snappedTo = "none"; 749 1202 snappedToX = 0; … … 765 1218 } 766 1219 767 function removeRightClicked(e) { 768 if (!e.target.classList.contains(' faded')) {1220 function removeRightClicked(e) { 1221 if (!e.target.classList.contains('disabled')) { 769 1222 removeRegion(); 770 1223 } 771 1224 } 772 1225 773 function replaceSelected(e) { 774 if (!e.target.classList.contains(' faded')) {1226 function replaceSelected(e) { // moves selected region across, replaces and removes any overlapping regions in the opposite set 1227 if (!e.target.classList.contains('disabled')) { 775 1228 let destinationSet = secondarySet; // replace down 776 1229 if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up … … 793 1246 } 794 1247 795 function containsRegion(set, region) { 1248 function containsRegion(set, region) { // true if given region exists in given set 796 1249 for (const item of set) { 797 1250 if (regionsMatch(region, item)) return true; … … 800 1253 } 801 1254 802 function overdubSelected(e) { 803 if (!e.target.classList.contains(' faded')) {1255 function overdubSelected(e) { // moves selected region across, merges any overlapping regions in the opposite set 1256 if (!e.target.classList.contains('disabled')) { 804 1257 let destinationSet = secondarySet; // replace down 805 1258 if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up 806 1259 let backup; 807 if (destinationSet.isSecondary) backup = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); // saves selected set as this process changes values in selected set (unknown reason)1260 if (destinationSet.isSecondary) backup = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); // saves selected set as this process changes values in selected set 808 1261 else backup = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects); 809 1262 copySelected(e, true); 810 1263 if (!currentRegions || currentRegions.length < 1) { // overdub single 811 handleSameSpeakerOverlap(getCurrentRegionIndex(), destinationSet );1264 handleSameSpeakerOverlap(getCurrentRegionIndex(), destinationSet, true); 812 1265 } else { // overdub multiple 813 1266 for (const item of getCurrentRegionsIndexes().reverse()) { // reverse indexes so index doesn't break when regions are removed 814 handleSameSpeakerOverlap(item, destinationSet );1267 handleSameSpeakerOverlap(item, destinationSet, true); 815 1268 } 816 1269 } … … 822 1275 } 823 1276 824 function copySelected(e, skipUndoState) { 825 if (!e.target.classList.contains('faded')) { 826 let out = -1; 1277 function copySelected(e, skipUndoState) { // copies region to opposite set [utility function for replace and overdub] 1278 if (!e.target.classList.contains('disabled')) { 827 1279 let destinationSet = secondarySet; // copy down 828 if (currSpeakerSet.isSecondary) { destinationSet = primarySet } // copy up 1280 if (currSpeakerSet.isSecondary) destinationSet = primarySet // copy up 1281 const selectedRegion = currentRegion; 829 1282 if (currentRegions && currentRegions.length > 1) { // copy multiple 830 const selectedRegion = currentRegion; 831 const selectedRegions = currentRegions; 832 destinationSet.tempSpeakerObjects.push(...selectedRegions); 833 currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions) 834 for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegions in dest. set 835 for (const selReg of selectedRegions) { 836 if (regionsMatch(reg, selReg) && !containsRegion(currentRegions, reg)) { 837 currentRegions.push(reg); 838 } 839 } 840 if (regionsMatch(reg, selectedRegion)) { currentRegion = reg; } 841 } 1283 destinationSet.tempSpeakerObjects.push(...selectedRegions); // append current regions to dest. set 1284 // currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions) 1285 // for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegions in dest. set 1286 // for (const selReg of selectedRegions) { 1287 // if (regionsMatch(reg, selReg) && !containsRegion(currentRegions, reg)) { 1288 // currentRegions.push(reg); 1289 // } 1290 // } 1291 // if (regionsMatch(reg, selectedRegion)) { currentRegion = reg; } 1292 // } 842 1293 } else { // copy singular 843 const selectedRegion = currentRegion; // copy currRegion as caretClicked wipes it844 1294 destinationSet.tempSpeakerObjects.push(selectedRegion); // append current region to dest. set 845 currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions)846 for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegion in dest. set847 if (regionsMatch(reg, selectedRegion)) {848 currentRegion = reg;849 break;850 }851 }1295 // currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions) 1296 // for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegion in dest. set 1297 // if (regionsMatch(reg, selectedRegion)) { 1298 // currentRegion = reg; 1299 // break; 1300 // } 1301 // } 852 1302 } 853 1303 reloadRegionsAndChapters(); … … 856 1306 } 857 1307 1308 /** 1309 * Shows context menu with various region options 1310 * @param {MouseEvent} e Either right click event or left click triple menu click event 1311 */ 858 1312 function onRightClick(e) { 859 if ( e.target.classList.contains("wavesurfer-region") && editMode) {1313 if ((e.target.classList.contains("wavesurfer-region") || e.target.id === "audioContainer" || e.target.classList.contains("chapter")) && editMode) { 860 1314 e.preventDefault(); 1315 e.stopPropagation(); 1316 // set current region to clicked region LLLLLLL 1317 let clickedRegion; // could be used to select clicked region 1318 for (const reg of currSpeakerSet.tempSpeakerObjects) { 1319 if (reg.region.element.title == e.target.title) { 1320 clickedRegion = reg; 1321 break; 1322 } 1323 } 1324 // console.log(clickedRegion) 861 1325 contextMenu.classList.add("visible"); 862 1326 if (e.clientX + 200 > $(window).width()) contextMenu.style.left = ($(window).width() - 220) + "px"; // ensure menu doesn't clip on right … … 864 1328 contextMenu.style.top = e.clientY + "px"; 865 1329 1330 let lockConflict = false; 1331 if (currentRegions.length > 1) { 1332 let firstIsLocked = 0; 1333 for (const reg of currentRegions) { 1334 if (firstIsLocked === 0) firstIsLocked = reg.locked; 1335 else if (firstIsLocked != reg.locked) lockConflict = true; 1336 } 1337 } 1338 if (lockConflict) { 1339 contextLock.classList.remove('disabled'); 1340 if (!document.getElementById("context-menu-lock-2")) { 1341 let contextLock2 = contextLock.cloneNode(); 1342 contextLock.innerText = "Lock Selected"; 1343 contextLock2.innerText = "Unlock Selected"; 1344 contextLock2.id = "context-menu-lock-2"; 1345 contextLock2.addEventListener('click', toggleLockSelected); 1346 contextLock.parentNode.insertBefore(contextLock2, contextLock.nextSibling); 1347 } 1348 } else { 1349 contextLock.classList.remove('disabled'); 1350 let currIndex = getCurrentRegionIndex(); 1351 if (currSpeakerSet.tempSpeakerObjects[currIndex] && currSpeakerSet.tempSpeakerObjects[currIndex].locked) { 1352 contextLock.innerText = "Unlock Selected"; 1353 chapters.childNodes[currIndex].childNodes[1].classList.remove('hide'); 1354 } else if (currSpeakerSet.tempSpeakerObjects[currIndex]) { 1355 contextLock.innerText = "Lock Selected"; 1356 chapters.childNodes[currIndex].childNodes[1].classList.add('hide'); 1357 } 1358 } 1359 866 1360 if (dualMode && currentRegion && currentRegion.speaker !== "") { 867 contextReplace.classList.remove('faded'); 868 contextOverdub.classList.remove('faded'); 869 // contextCopy.classList.remove('faded'); 1361 contextReplace.classList.remove('disabled'); 1362 contextOverdub.classList.remove('disabled'); 870 1363 } else { 871 contextDelete.classList.add('faded'); 872 contextReplace.classList.add('faded'); 873 contextOverdub.classList.add('faded'); 874 // contextCopy.classList.add('faded'); 875 } 876 if (currentRegion && currentRegion.speaker !== "") contextDelete.classList.remove('faded'); 1364 contextDelete.classList.add('disabled'); 1365 contextLock.classList.add('disabled'); 1366 contextReplace.classList.add('disabled'); 1367 contextOverdub.classList.add('disabled'); 1368 } 1369 if (currentRegion && currentRegion.speaker !== "") { 1370 contextDelete.classList.remove('disabled'); 1371 contextLock.classList.remove('disabled'); 1372 } 877 1373 if (dualMode) { // manipulate context texts 878 1374 const actionDirection = currSpeakerSet.isSecondary ? "Up" : "Down"; 879 1375 contextReplace.innerHTML = "Replace Selected " + actionDirection; 880 1376 contextOverdub.innerHTML = "Overdub Selected " + actionDirection; 881 // contextCopy.innerHTML = "Copy Selected " + actionDirection;882 1377 } 883 1378 } … … 891 1386 } 892 1387 893 function keyUp(e) { 1388 function keyUp(e) { // key up listener 894 1389 if (e.key == "Control") ctrlDown = false; 895 1390 if (e.target.tagName !== "INPUT") { 896 1391 if (e.code === "Backspace" || e.code === "Delete") removeRegion(); 897 else if (e.code === "Space") wavesurfer.playPause();1392 else if (e.code === "Space") { wavesurfer.playPause(); } 898 1393 else if (e.code === "ArrowLeft") wavesurfer.skipBackward(); 899 1394 else if (e.code === "ArrowRight") wavesurfer.skipForward(); 1395 else if (e.code === "KeyL") toggleLockSelected(e); 900 1396 } 901 1397 if (e.code == "KeyZ" && e.ctrlKey) undo(); … … 903 1399 } 904 1400 905 function keyDown(e) { 1401 function keyDown(e) { // keydown listener 906 1402 if (e.key == "Control") ctrlDown = true; 907 } 908 909 function dualModeChanged(skipUndoState) { // on dualmode checkbox value change 910 clearChapterSearch(); 1403 if (e.code == "Space" && e.target.tagName.toLowerCase() != "input") e.preventDefault(); 1404 } 1405 1406 /** 1407 * Shows / hides secondary speaker set 1408 * @param skipUndoState Utility param - skips the addition of an undo state 1409 * @param overrideValue Utility param - overrides the checkbox state 1410 */ 1411 function dualModeChanged(skipUndoState, overrideValue) { 1412 if (overrideValue) dualModeCheckbox.checked = overrideValue == "true" ? true : false; 1413 else dualModeCheckbox.checked = !dualModeCheckbox.checked; // toggle dual mode checkbox 911 1414 dualMode = dualModeCheckbox.checked; 912 1415 currSpeakerSet = primarySet; 913 1416 if (!dualMode) removeCurrentRegion(); 1417 clearChapterSearch(); 914 1418 reloadRegionsAndChapters(); 915 1419 if (dualMode) { 916 dualModeMenuButton.classList.add('visible');917 1420 if (!secondaryLoaded) { 918 loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet); 1421 const secondaryCSVURL = "http://localhost:8383/greenstone3/cgi-bin/metadata-server.pl?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.collectionMetadata.indexStem + 1422 "&d=" + gs.documentMetadata.Identifier + "&assocname=structured-audio.csv&dv=nminus-1"; 1423 loadCSVFile(secondaryCSVURL, secondarySet); 919 1424 secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once 920 1425 } 921 1426 document.getElementById("caret-container").style.display = "flex"; 1427 timelineMenuRegionConflict.classList.remove("disabled"); 1428 timelineMenuSpeakerConflict.classList.remove("disabled"); 1429 $('#track-set-label-bottom').fadeIn(100); 1430 selectedVersions[1] = document.getElementById('track-set-label-bottom').children[0].innerText; 922 1431 } else { 923 dualModeMenuButton.classList.remove('visible');924 1432 caretClicked('primary-caret'); 925 1433 document.getElementById("caret-container").style.display = "none"; 1434 selectedVersions.splice(1, 1); // trim to one version in array 1435 timelineMenuRegionConflict.firstElementChild.checked = false; 1436 timelineMenuSpeakerConflict.firstElementChild.checked = false; 1437 timelineMenuRegionConflict.classList.add("disabled"); 1438 timelineMenuSpeakerConflict.classList.add("disabled"); 1439 $('#track-set-label-bottom').fadeOut(100); 926 1440 } 927 1441 currSpeakerSet = primarySet; 928 drawVersionNames();929 1442 if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dualModeChange", getCurrentRegionIndex()); 930 1443 } 931 1444 932 // path to toolbar images 933 let interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; 934 1445 /** 1446 * Changes selected speaker set 1447 * @param {string} id ID of clicked caret image 1448 */ 935 1449 function caretClicked(id) { 936 1450 clearChapterSearch(); … … 944 1458 } 945 1459 1460 /** 1461 * Loads destination waveform and audio if required, updates caret images 1462 * @param {boolean} toPrimary whether destination set is primary (true) or secondary (false) 1463 */ 946 1464 function swapCarets(toPrimary) { 947 const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false; 1465 const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false; // initial value before swap 948 1466 if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) { 949 1467 removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet … … 952 1470 } 953 1471 if (toPrimary) { 1472 if (!currCaretIsPrimary) { 1473 showAudioLoader(); 1474 if (canvasImages[selectedVersions[0]]) { // if waveform image exists in cache 1475 drawImageOnWaveform(canvasImages[selectedVersions[0]]); 1476 // hideAudioLoader(); 1477 } 1478 // else showAudioLoader(); 1479 let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + 1480 "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio; 1481 if (selectedVersions[0] !== "current") { 1482 if (selectedVersions[0].includes("Previous")) url += "&dv=" + selectedVersions[0].replace("Previous(", "nminus-").replace(")", ""); 1483 else url += "&dv=" + selectedVersions[0]; 1484 } 1485 wavesurfer.load(url); 1486 } 954 1487 primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg"; 955 1488 secondaryCaret.src = interface_bootstrap_images + "caret-right.svg"; 956 1489 } else { 1490 if (currCaretIsPrimary) { 1491 showAudioLoader(); 1492 if (canvasImages[selectedVersions[1]]) { 1493 drawImageOnWaveform(canvasImages[selectedVersions[1]]); 1494 // hideAudioLoader(); 1495 } 1496 // else showAudioLoader(); 1497 let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + 1498 "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio; 1499 if (selectedVersions[1] !== "current") { 1500 if (selectedVersions[1].includes("Previous")) url += "&dv=" + selectedVersions[1].replace("Previous(", "nminus-").replace(")", ""); 1501 else url += "&dv=" + selectedVersions[1]; 1502 } 1503 wavesurfer.load(url); 1504 } 957 1505 primaryCaret.src = interface_bootstrap_images + "caret-right.svg"; 958 1506 secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg"; … … 960 1508 } 961 1509 1510 /** 1511 * Shows spinning loader over waveform, hides regions 1512 */ 1513 function showAudioLoader() { 1514 $('.wavesurfer-region').fadeOut(100); 1515 $(".chapter").fadeOut(100); 1516 $(".track-set-label").fadeOut(100); 1517 waveformSpinner.style.display = 'block'; 1518 loader.style.display = "inline"; 1519 for (const ele of editPanel.children) ele.classList.add("disabled"); 1520 playPauseButton.classList.add("disabled"); 1521 } 1522 1523 /** 1524 * Hides spinning loader, brings back regions 1525 */ 1526 function hideAudioLoader() { 1527 $('.wavesurfer-region').fadeIn(100); 1528 $(".chapter").fadeIn(100); 1529 $("#track-set-label-top").fadeIn(100); 1530 if (dualMode) $('#track-set-label-bottom').fadeIn(100); 1531 waveformSpinner.style.display = 'none'; 1532 loader.style.display = "none"; 1533 for (const ele of editPanel.children) ele.classList.remove("disabled"); 1534 updateRegionEditPanel(); 1535 playPauseButton.classList.remove("disabled"); 1536 } 1537 1538 /** 1539 * Draws given image URL on waveform 1540 * @param image URL of image to be drawn 1541 */ 1542 function drawImageOnWaveform(image) { 1543 // console.log('draw waveform image from cache') 1544 if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove(); 1545 var newCanvas = document.createElement("div"); 1546 newCanvas.id = "new-canvas"; 1547 newCanvas.style.width = wavesurfer.drawer.canvases[0].wave.width + 'px'; 1548 newCanvas.style.height = '140px'; 1549 newCanvas.style.backgroundImage = "url('" + image + "')"; 1550 waveformContainer.appendChild(newCanvas); 1551 } 1552 1553 /** 1554 * Regenerates chapter list to update any changes made in speakerSet 1555 */ 962 1556 function reloadChapterList() { 963 1557 chapters.innerHTML = ""; … … 969 1563 speakerName.classList.add("speakerName"); 970 1564 speakerName.innerText = currSpeakerSet.tempSpeakerObjects[i].speaker; 1565 let regionLocked = document.createElement("img"); 1566 regionLocked.src = interface_bootstrap_images + "lock.svg"; 1567 regionLocked.classList.add("speakerLocked", "hide"); 1568 attachPadlockListener(regionLocked, currSpeakerSet.tempSpeakerObjects[i].region, true); 1569 if (currSpeakerSet.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide"); 971 1570 let speakerTime = document.createElement("span"); 972 1571 speakerTime.classList.add("speakerTime"); 973 1572 speakerTime.innerHTML = minutize(currSpeakerSet.tempSpeakerObjects[i].start) + " - " + minutize(currSpeakerSet.tempSpeakerObjects[i].end) + "s"; 974 1573 chapter.appendChild(speakerName); 1574 chapter.appendChild(regionLocked); 975 1575 chapter.appendChild(speakerTime); 976 1576 chapter.addEventListener("click", chapterClicked); … … 985 1585 } 986 1586 987 wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; }); 988 wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; }); 989 wavesurfer.on("mute", function(mute) { 990 if (mute) { 991 muteButton.src = interface_bootstrap_images + "mute.svg"; 992 muteButton.style.opacity = 0.6; 993 volumeSlider.value = 0; 994 } 995 else { 996 muteButton.src = interface_bootstrap_images + "unmute.svg"; 997 muteButton.style.opacity = 1; 998 volumeSlider.value = 1; 999 } 1000 }); 1001 1002 volumeSlider.addEventListener("input", function() { 1003 wavesurfer.setVolume(this.value); 1004 if (this.value == 0) { 1005 muteButton.src = interface_bootstrap_images + "mute.svg"; 1006 muteButton.style.opacity = 0.6; 1007 } else { 1008 muteButton.src = interface_bootstrap_images + "unmute.svg"; 1009 muteButton.style.opacity = 1; 1010 } 1011 }); 1012 1013 zoomSlider.addEventListener("input", function() { // slider changes waveform zoom 1014 wavesurfer.zoom(Number(this.value) / 4); 1015 if (currentRegion.speaker && getCurrentRegionIndex() != -1) { 1016 setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); 1017 drawCurrentRegionBounds(); 1018 } 1019 let handles = document.getElementsByClassName("wavesurfer-handle"); 1020 if (this.value < 20) { 1021 for (const handle of handles) { 1022 handle.style.setProperty("width", "1px", "important"); 1023 } 1024 } else { 1025 for (const handle of handles) { 1026 handle.style.setProperty("width", "3px", "important"); 1027 } 1028 } 1029 }); 1030 wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point 1031 1032 let toggleChapters = function() { // show & hide chapter section 1587 /** 1588 * Shows / hides chapter section 1589 */ 1590 let toggleChapters = function() { 1033 1591 if (chapters.style.height == "0px") { 1034 1592 chapters.style.height = "90%"; … … 1042 1600 } 1043 1601 1602 /** 1603 * Object representing elements of a diarization output 1604 * @param {boolean} isSecondary Whether or not the set is secondary/bottom (true) or primary/top (false) 1605 * @param {Array} uniqueSpeakers Array of all unique speaker names within the diarization data, used for colouring regions 1606 * @param {Array} speakerObjects Array of objects containing speaker start/stop times and names 1607 * @param {Array} tempSpeakerObjects Temporary version of speakerObjects, which can be reverted back to if required 1608 */ 1044 1609 function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) { 1045 1610 this.isSecondary = isSecondary; … … 1048 1613 this.tempSpeakerObjects = tempSpeakerObjects; 1049 1614 } 1050 let primarySet = new SpeakerSet(false, [], [], []); 1051 let secondarySet = new SpeakerSet(true, [], [], []); 1615 1616 let primarySet = new SpeakerSet(false, [], [], [], []); 1617 let secondarySet = new SpeakerSet(true, [], [], [], []); 1052 1618 let currSpeakerSet = primarySet; 1053 1619 1054 function loadCSVFile(filename, manualHeader, speakerSet) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript 1620 /** 1621 * Reads diarization CSV file and populates speakerSet 1622 * @param {string} filename Source destination of input CSV file 1623 * @param {object} speakerSet speaker set to be populated 1624 * @param {boolean} forcePopulate Forces redraw of regions and chapters 1625 */ 1626 function loadCSVFile(filename, speakerSet, forcePopulate) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript 1055 1627 $.ajax({ 1056 1628 type: "GET", … … 1060 1632 let dataLines = data.split(/\r\n|\n/); 1061 1633 let headers; 1062 let startIndex ;1634 let startIndex = 0; 1063 1635 speakerSet.uniqueSpeakers = []; // used for obtaining unique colours 1064 1636 speakerSet.speakerObjects = []; // list of speaker items 1065 1637 1066 if (manualHeader) { // headers for columns can be provided if not existent in csv 1067 headers = manualHeader; 1068 startIndex = 0; 1069 } else { 1070 headers = dataLines[0].split(','); 1071 startIndex = 1; 1072 } 1638 if (dataLines[0].split(',').length === 3) headers = ["speaker", "start", "end"]; // assume speaker, start, end 1639 else if (dataLines[0].split(',').length === 4) headers = ["speaker", "start", "end", "locked"]; // assume speaker, start, end, locked 1073 1640 1074 1641 for (let i = startIndex; i < dataLines.length; i++) { … … 1082 1649 } 1083 1650 } 1651 if (headers.length === 3) item['locked'] = false; 1084 1652 speakerSet.speakerObjects.push(item); 1085 1653 } 1086 1654 } 1087 1655 speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects); 1088 populateChapters(speakerSet);1656 if (!speakerSet.isSecondary || forcePopulate) populateChaptersAndRegions(speakerSet); // prevents secondary set being drawn on first load 1089 1657 resetUndoStates(); // undo stack init 1090 }); 1091 } 1092 1093 function populateChapters(data) { // populates chapter section and adds regions to waveform 1658 }, (error) => { console.log("loadCSVFile error:"); console.log(error); }); 1659 } 1660 1661 /** 1662 * Populates chapter list div and regions on waveform with given speaker set 1663 * @param {object} data Speaker set object with diarization data 1664 */ 1665 function populateChaptersAndRegions(data) { 1094 1666 // colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings. 1095 1667 // this colour scheme is designed for qualitative data 1096 1097 1668 if (regionColourSet.length < 1) { 1098 1669 for (let i = 0; i < data.uniqueSpeakers.length; i++) { // not tested in cases where there are more than 8 speakers!! … … 1107 1678 data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time 1108 1679 if (isSelectedSet || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population 1109 1110 1680 for (let i = 0; i < data.tempSpeakerObjects.length; i++) { 1111 1681 let chapter = document.createElement("div"); … … 1115 1685 speakerName.classList.add("speakerName"); 1116 1686 speakerName.innerText = data.tempSpeakerObjects[i].speaker; 1687 let regionLocked = document.createElement("img"); 1688 regionLocked.src = interface_bootstrap_images + "lock.svg"; 1689 regionLocked.classList.add("speakerLocked", "hide"); 1690 attachPadlockListener(regionLocked, data.tempSpeakerObjects[i].region, true); 1691 if (data.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide"); 1117 1692 let speakerTime = document.createElement("span"); 1118 1693 speakerTime.classList.add("speakerTime"); 1119 1694 speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s"; 1120 1695 chapter.appendChild(speakerName); 1696 chapter.appendChild(regionLocked); 1121 1697 chapter.appendChild(speakerTime); 1122 1698 chapter.addEventListener("click", chapterClicked); … … 1151 1727 label: speakerName, 1152 1728 }, 1153 // color: colourbrewerSet[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,1154 1729 color: regColour + regionTransparency, 1155 1730 ...(selected) && {color: "rgba(255,50,50,0.5)"}, 1156 1731 }); 1157 1732 data.tempSpeakerObjects[i].region = associatedReg; 1158 } 1159 1733 if (selected && data.tempSpeakerObjects[i].locked) { // add padlock to regions if they are selected and locked 1734 let lock = drawPadlock(associatedReg.element); 1735 attachPadlockListener(lock, associatedReg, false); 1736 } 1737 if (selected) drawMenuButton(associatedReg); 1738 } 1739 if (waveformSpinner.style.display == 'block') $(".wavesurfer-region").fadeOut(100); // keep regions hidden until wavesurfer.load() has finished 1160 1740 let handles = document.getElementsByTagName('handle'); 1161 1741 for (const handle of handles) handle.addEventListener('mousedown', () => mouseDown = true); … … 1163 1743 let regions = document.getElementsByTagName("region"); 1164 1744 if (dualMode) { 1165 if (document.getElementsByClassName("region-top").length === 0) for (const reg of regions) reg.classList.add("region-top"); 1166 else for (const rego of regions) if (!rego.classList.contains("region-top")) rego.classList.add("region-bottom"); 1745 if (document.getElementsByClassName("region-top").length == 0) { 1746 for (const reg of regions) { 1747 if (reg.classList.length == 1) reg.classList.add("region-top"); 1748 } 1749 } else { 1750 for (const rego of regions) { 1751 if (!rego.classList.contains("region-top") && rego.classList.length == 1) rego.classList.add("region-bottom"); 1752 } 1753 } 1167 1754 } 1168 1755 if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important"); … … 1177 1764 url: filename, 1178 1765 dataType: "text", 1179 }).then(function(data){ populateWords(JSON.parse(data)) } );1766 }).then(function(data){ populateWords(JSON.parse(data)) }, (error) => { console.log("loadJSONFile error:"); console.log(error); }); 1180 1767 } 1181 1768 … … 1239 1826 } 1240 1827 } 1241 1828 /** 1829 * Handles region and chapter colours 1830 * @param {object} region Region element to adjust 1831 * @param {boolean} highlight Whether or not region should be white-highlighted 1832 */ 1242 1833 function handleRegionColours(region, highlight) { // handles region, chapter & word colours 1243 1834 if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) { 1244 1835 let colour; 1245 1836 if (highlight) { 1246 colour = "rgb( 101, 116, 116)";1837 colour = "rgb(81, 90, 90)"; 1247 1838 regionEnter(region); 1248 1839 } else { … … 1253 1844 colour = "rgba(255, 50, 50, 0.5)"; 1254 1845 } 1255 chapters.childNodes[region.id.replace("region","")].style.backgroundColor = colour;1846 if (chapters.childNodes[getIndexOfRegion(region)]) chapters.childNodes[getIndexOfRegion(region)].style.backgroundColor = colour; 1256 1847 } 1257 1848 } 1258 1849 1259 1850 function regionEnter(region) { 1260 // console.log("regionEnter");1261 1851 if (isCurrentRegion(region) || isInCurrentRegions(region)) { 1262 1852 region.update({ color: "rgba(255, 50, 50, 0.5)" }); 1263 1853 } else { 1264 region.update({ color: "rgba(255, 255, 255, 0.35)" }); 1854 region.update({ color: "rgba(255, 255, 255, 0.3)" }); 1855 } 1856 if (editMode && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)] && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)].locked 1857 && region.element.getElementsByClassName("region-padlock").length == 0) { // hovered region is locked 1858 let lock = drawPadlock(region.element); 1859 attachPadlockListener(lock, region, false); 1860 } 1861 if (editMode && region.element.getElementsByClassName("region-menu").length == 0) { 1862 drawMenuButton(region); 1265 1863 } 1266 1864 } … … 1270 1868 if (isCurrentRegion(region) || isInCurrentRegions(region)) { 1271 1869 region.update({ color: "rgba(255, 50, 50, 0.5)" }); 1272 } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { 1870 // } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { 1871 } else { 1273 1872 let index = region.id.replace("region", ""); 1274 1873 region.update({ color: regionColourSet.find(item => item.name === currSpeakerSet.tempSpeakerObjects[index].speaker).colour + regionTransparency }); 1874 } 1875 if (region.element.getElementsByTagName("img").length > 0 && !isCurrentRegion(region) && !isInCurrentRegions(region)) { 1876 for (let child of Array.from(region.element.children)) { 1877 if (child.tagName == "IMG") { 1878 child.remove(); 1879 } 1880 } 1275 1881 } 1276 1882 } else { … … 1291 1897 1292 1898 function getLetter(val) { 1293 // return val.replace("SPEAKER_","");1294 1899 let speakerNum = parseInt(val.replace("SPEAKER_","")); 1295 1900 return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65 1296 1901 } 1297 1902 1298 1299 1300 // edit functionality 1301 1302 function toggleEditMode() { // toggles edit panel and redraws regions with resize handles 1903 function toggleEditMode(skipDualModeToggle) { // toggles edit panel and redraws regions with resize handles 1303 1904 if (gs.variables.allowEditing === '1') { 1304 if (dualMode) dualModeCheckbox.click(); // dual mode is disabled when leaving edit mode1305 1905 toggleEditPanel(); 1306 1906 updateRegionEditPanel(); 1307 drawVersionNames(); 1308 } 1309 } 1310 1311 function drawVersionNames() { 1312 if (document.getElementById("prim-set-label")) document.getElementById("prim-set-label").remove(); 1313 if (document.getElementById("sec-set-label")) document.getElementById("sec-set-label").remove(); 1314 if (editMode && !document.body.contains(loader)) { // editmode is opposite here 1315 let dataLabel = document.createElement("span"); 1316 dataLabel.textContent = gs.documentMetadata.Title + " V1.0"; 1317 dataLabel.id = "prim-set-label"; 1318 waveformContainer.prepend(dataLabel); 1319 if (dualMode) { 1320 let dataLabel = document.createElement("span"); 1321 dataLabel.textContent = gs.documentMetadata.Title + " V2.0"; 1322 dataLabel.id = "sec-set-label"; 1323 waveformContainer.prepend(dataLabel); 1324 } 1325 } 1907 reloadChapterList(); 1908 } 1909 } 1910 1911 function toggleVersionDropdown(e) { 1912 e.stopPropagation(); 1913 if (versionSelectMenu.classList.contains("visible")) { 1914 e.target.style.display = 'inline'; 1915 versionSelectMenu.classList.remove("visible"); 1916 } 1917 else { 1918 e.target.style.display = 'none'; 1919 versionSelectMenu.classList.add("visible"); 1920 versionSelectMenu.style.top = "2rem"; 1921 versionSelectMenu.style.height = wave.clientHeight + wavesurfer.timeline.container.clientHeight + document.getElementById("audio-toolbar").clientHeight - 6 + "px"; 1922 if (e.target.parentElement.id.includes("top")) versionSelectMenu.classList.add("versionTop"); 1923 else versionSelectMenu.classList.remove("versionTop"); 1924 for (version of versionSelectMenu.children) { // handle disabling of regions if being viewed 1925 if (selectedVersions.includes(version.id) || selectedVersions.includes(version.innerText)) version.classList.add('disabled'); 1926 else version.classList.remove('disabled'); 1927 } 1928 } 1326 1929 } 1327 1930 … … 1330 1933 hoverSpeaker.innerHTML = ""; 1331 1934 if (editPanel.style.height == "0px") { 1332 if (chapters.style.height == "0px") { // expands chapter panel 1333 toggleChapters(); 1334 } 1935 if (chapters.style.height == "0px") toggleChapters(); // expands chapter panel 1335 1936 editPanel.style.height = "30vh"; 1336 editPanel.style.padding = " 1rem";1937 editPanel.style.padding = "0.5rem"; 1337 1938 setRegionEditMode(true); 1338 1939 } else { … … 1343 1944 } 1344 1945 1345 function setRegionEditMode(state) { 1946 function setRegionEditMode(state) { 1346 1947 editMode = state; 1347 1948 chapters.innerHTML = ''; 1348 wavesurfer.clearRegions(); 1349 populateChapters(currSpeakerSet); 1350 } 1351 1949 $('.wavesurfer-region').hide(); 1950 reloadRegionsAndChapters(); // editMode sets drag/resize property when regions are redrawn 1951 } 1952 1953 /** 1954 * Handles the edit of region start time, stop time, or speaker name, updating the speaker set 1955 * @param {object} region Region that has been updated 1956 */ 1352 1957 function handleRegionEdit(region, e) { 1353 1958 if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) } … … 1355 1960 editsMade = true; 1356 1961 currentRegion = region; 1357 region.play(); 1358 wavesurfer.pause(); 1962 wavesurfer.backend.seekTo(region.start); 1359 1963 let regionIndex = getCurrentRegionIndex(); 1360 1964 currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker; … … 1369 1973 handleSameSpeakerOverlap(getCurrentRegionIndex(), currSpeakerSet); // recalculate index in case start pos has changed 1370 1974 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dragdrop", getCurrentRegionIndex()); 1975 editLockedRegion(currSpeakerSet.tempSpeakerObjects[regionIndex], chaps); 1976 1371 1977 editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!) 1372 1978 } 1373 1979 1374 function handleSameSpeakerOverlap(regionIdx, speakerSet) { // consumes/merges same-speaker regions with overlapping bounds 1980 /** 1981 * Shows popup to ensure user is aware they are editing a locked region 1982 * @param {object} region Region that is being edited 1983 */ 1984 function editLockedRegion(region) { // ensures user is aware region being edited is locked 1985 if (region.locked) { 1986 let confirm = false; 1987 confirm = window.confirm("Editing a locked region will unlock it, are you sure you want to continue?"); 1988 if (!confirm) undo(); // undo change if no 1989 else { // remove lock if yes 1990 region.locked = false; 1991 if (region.region && region.region.element.firstChild) region.region.element.firstChild.remove(); // remove region padlock 1992 if (chapters.childNodes[getCurrentRegionIndex()] && chapters.childNodes[getCurrentRegionIndex()].childNodes[1].tagName === "IMG") { 1993 chapters.childNodes[getCurrentRegionIndex()].childNodes[1].classList.add('hide'); // remove chapter padlock 1994 } 1995 } 1996 } 1997 } 1998 1999 /** 2000 * Merges same-speaker regions with overlapping bounds 2001 * @param {int} regionIdx Index of dragged/edited region 2002 * @param {object} speakerSet Speaker set dragged region exists in 2003 * @param {boolean} skipCurrentRegionUpdate Whether or not to skip the updating of current region 2004 */ 2005 function handleSameSpeakerOverlap(regionIdx, speakerSet, skipCurrentRegionUpdate) { 1375 2006 let draggedRegion = speakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here 1376 2007 let draggedRegionSpeaker = draggedRegion.speaker; … … 1381 2012 draggedRegion.end = Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end); 1382 2013 draggedRegion.region.update({start: Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start), end: Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end)}); 1383 currentRegion = draggedRegion;2014 if (!skipCurrentRegionUpdate) currentRegion = draggedRegion; 1384 2015 speakerSet.tempSpeakerObjects[i].region.remove(); 1385 2016 speakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region … … 1401 2032 } 1402 2033 1403 function updateRegionEditPanel() { // updates edit panel content/inputs 1404 // console.log('updating regionEditPanel') 2034 /** 2035 * Updates the edit panel elements based on various editing states 2036 */ 2037 function updateRegionEditPanel() { 1405 2038 if (currentRegion && currentRegion.speaker == "") { 1406 2039 removeButton.classList.add("disabled"); … … 1441 2074 } 1442 2075 2076 /** 2077 * Adds a new region to the waveform at the current caret location with the speaker name "NEW_SPEAKER" 2078 */ 1443 2079 function createNewRegion() { // adds a new region to the waveform 1444 2080 clearChapterSearch(); … … 1447 2083 const start = newRegionOffset + wavesurfer.getCurrentTime(); 1448 2084 const end = newRegionOffset + wavesurfer.getCurrentTime() + 15; 1449 newRegionOffset += 5; // offset new region if multiple new regions are created. TODO: check region has different start time2085 newRegionOffset += 5; // offset new region if multiple new regions are created. 1450 2086 currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end}); 1451 2087 … … 1467 2103 } 1468 2104 1469 function removeRegion() { // removes currently selected region or regions 2105 /** 2106 * Removes the currently selected region or regions 2107 */ 2108 function removeRegion() { 1470 2109 if (!removeButton.classList.contains("disabled")) { 1471 2110 if (getCurrentRegionIndex() != -1) { // if currentRegion has been set 1472 2111 let currentRegionIndex = getCurrentRegionIndex(); 1473 2112 let currentRegionIndexes = getCurrentRegionsIndexes(); 2113 let lockTemplate = { locked: currSpeakerSet.tempSpeakerObjects[currentRegionIndex].locked }; 1474 2114 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { 1475 2115 if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { … … 1482 2122 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex); 1483 2123 updateRegionEditPanel(); 2124 reloadChapterList(); 2125 editLockedRegion(lockTemplate); 1484 2126 return; // jump out of function 1485 2127 } … … 1493 2135 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed 1494 2136 updateRegionEditPanel(); 2137 reloadChapterList(); 2138 editLockedRegion(lockTemplate); 1495 2139 } else { console.log("no region selected") } 1496 2140 } … … 1554 2198 } 1555 2199 1556 function speakerChange() { // speaker input name onInput handler 2200 /** 2201 * Changes the associated speaker name of a region, updating the speaker set 2202 */ 2203 function speakerChange() { 1557 2204 const newSpeaker = speakerInput.value; 1558 2205 clearChapterSearch(); 1559 if (newSpeaker && newSpeaker != "") {2206 if (newSpeaker && newSpeaker.trim() != "") { 1560 2207 speakerInput.style.outline = "2px solid transparent"; 1561 2208 if (getCurrentRegionIndex() != -1) { // if a region is selected … … 1577 2224 editsMade = true; 1578 2225 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes()); 2226 editLockedRegion(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()]); 1579 2227 } else { console.log("no region selected") } 1580 2228 } else { console.log("no text in speaker input"); speakerInput.style.outline = "2px solid firebrick"; } … … 1590 2238 } 1591 2239 2240 /** 2241 * Selects all (or reverts select-all) regions matching any of the currently selected speaker names 2242 * @param {boolean} skipUndoState Whether or not to skip the addition of an undo state 2243 */ 1592 2244 function selectAllCheckboxChanged(skipUndoState) { // "Change all" toggled 1593 2245 if (changeAllCheckbox.checked) { … … 1624 2276 1625 2277 function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs 1626 for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = true; startTimeInput.childNodes[idx].value = 0; } 1627 for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = true; endTimeInput.childNodes[idx].value = 0; } 1628 } 1629 1630 function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination 2278 for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].value = 0; startTimeInput.childNodes[idx].disabled = true; } 2279 for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].value = 0; endTimeInput.childNodes[idx].disabled = true; } 2280 } 2281 2282 /** 2283 * Zooms wavesurfer waveform to destination zoom level, used in select all function 2284 * @param {number} dest Destination zoom level 2285 */ 2286 function zoomTo(dest) { 1631 2287 isZooming = true; 1632 2288 changeAllCheckbox.disabled = true; … … 1657 2313 } 1658 2314 } 1659 }, 10); // interval 1660 2315 }, 10); // 10ms interval 2316 } 2317 2318 function toggleSavePopup() { // shows / hides commit popup div 2319 savePopupCommitMsg.value = savePopupCommitMsg.value.trim(); // clears initial whitespace caused by <xsl: text> 2320 if (savePopup.classList.contains("visible")) { 2321 savePopup.classList.remove("visible"); 2322 savePopupBG.classList.remove("visible"); 2323 } else { 2324 savePopup.classList.add("visible"); 2325 savePopupBG.classList.add("visible"); 2326 savePopup.children[0].innerText = "Commit changes for: " + selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1]; 2327 } 1661 2328 } 1662 2329 1663 2330 function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects 1664 2331 if (!saveButton.classList.contains("disabled")) { 1665 currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects); 1666 editsMade = false; 1667 removeCurrentRegion(); 1668 reloadRegionsAndChapters(); 1669 console.log("saved changes"); 1670 } 1671 } 1672 1673 function discardRegionChanges() { // resets tempSpeakerObjects to speakerObjects 1674 if (!discardButton.classList.contains("disabled")) { 1675 let confirm = window.confirm("Are you sure you want to discard changes?"); 1676 if (confirm) { 2332 toggleSavePopup(); 2333 // old save functionality 2334 // currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects); 2335 // editsMade = false; 2336 // removeCurrentRegion(); 2337 // reloadRegionsAndChapters(); 2338 // console.log("saved changes."); 2339 } 2340 } 2341 2342 /** 2343 * Commits changes made to the currently selected set to Greenstone's version history system. 2344 * Firstly increments FLDV, then saves commit message to document's metadata, then sets document's 2345 * associated file to tempSpeakerObjects CSV. 2346 */ 2347 function commitChanges() { 2348 if (savePopupCommitMsg.value && savePopupCommitMsg.value.length > 0) { 2349 console.log('committing with message: ' + savePopupCommitMsg.value); 2350 // inc fldv_history 2351 $.ajax({ 2352 type: "GET", 2353 url: mod_meta_base_url, 2354 data: { "o": "json", "s1.a": "inc-fldv-nminus1" } 2355 }).then((out) => { 2356 console.log('fldv inc success with status code: ' + out.page.pageResponse.status.code); 2357 if (out.page.pageResponse.status.code == 11) { // more information on codes found in: GSStatus.java 2358 ajaxSetCommitMeta(); 2359 } 2360 }, (error) => { console.log("inc-fldv-nminus1 error:\n" + error) }); 2361 toggleSavePopup(); 2362 } else { 2363 window.alert("Commit message cannot be left empty."); 2364 } 2365 } 2366 2367 function ajaxSetCommitMeta() { // saves commit message to current document's metadata 2368 $.ajax({ 2369 type: "GET", 2370 url: mod_meta_base_url, 2371 data: { "o" : "json", "s1.a": "set-archives-metadata", "s1.metaname": "commitmessage", "s1.metavalue": savePopupCommitMsg.value.trim(), "s1.metamode": "override" }, 2372 }).then((out) => { 2373 console.log('commit success with status code: ' + out.page.pageResponse.status.code); 2374 if (out.page.pageResponse.status.code == 11) { 2375 ajaxSetAssocFile(); 2376 } 2377 }, (error) => { console.log("commit_msg_url error:"); console.log(error); }); 2378 } 2379 2380 function ajaxSetAssocFile() { // sets current document's associated file to tempSpeakerObjects 2381 $.ajax({ 2382 type: "POST", 2383 url: gs.xsltParams.library_name, 2384 data: { "o" : "json", "a": "g", "rt": "r", "ro": "0", "s": "ModifyMetadata", "s1.collection": gs.cgiParams.c, "s1.site": gs.xsltParams.site_name, "s1.d": gs.cgiParams.d, 2385 "s1.a": "set-archives-assocfile", "s1.assocname": "structured-audio.csv", "s1.filedata": speakerObjToCSVText() }, 2386 }).then((out) => { 2387 console.log('set-archives-assocfile success with status code: ' + out.page.pageResponse.status.code); 2388 resetUndoStates(); 2389 }, (error) => { console.log("set_assoc_url error:"); console.log(error); }); 2390 } 2391 2392 function speakerObjToCSVText() { // converts tempSpeakerObjects to csv-like string 2393 console.log(currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n')); 2394 return currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n'); 2395 } 2396 2397 function discardRegionChanges(forceDiscard) { // resets tempSpeakerObjects to speakerObjects 2398 if (!discardButton.classList.contains("disabled") || forceDiscard) { 2399 let confirm = false; 2400 if (!forceDiscard) { confirm = window.confirm("Are you sure you want to discard changes?"); } 2401 if (confirm || forceDiscard) { 1677 2402 currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects); 1678 2403 editsMade = false; … … 1685 2410 } 1686 2411 2412 /** 2413 * Redraws edit panel, chapter list and wavesurfer regions from speaker set 2414 */ 1687 2415 function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions 1688 2416 updateRegionEditPanel(); 1689 wavesurfer.clearRegions();1690 2417 $(".region-top").remove(); 1691 2418 $(".region-bottom").remove(); 1692 2419 $(".wavesurfer-region").remove(); 1693 populateChapters (primarySet);2420 populateChaptersAndRegions(primarySet); 1694 2421 if (dualMode) { 1695 populateChapters (secondarySet);2422 populateChaptersAndRegions(secondarySet); 1696 2423 currSpeakerSet = primarySet; 1697 2424 } … … 1703 2430 if (currentRegions.length < 1) { 1704 2431 removeButton.innerHTML = "Remove Selected Region"; 1705 enableStartEndInputs();2432 // enableStartEndInputs(); 1706 2433 } else { 1707 2434 removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")"; 1708 disableStartEndInputs();1709 2435 const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions 1710 2436 uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); … … 1713 2439 } 1714 2440 2441 /** 2442 * Handles the change of a region's start or end time, updating hte speaker set 2443 */ 1715 2444 function changeStartEndTime(e) { // start/end time input handler 1716 2445 let newStart = getTimeInSecondsFromInput(startTimeInput); … … 1735 2464 handleSameSpeakerOverlap(currRegIdx, currSpeakerSet); 1736 2465 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "change-time", getCurrentRegionIndex()); 2466 editLockedRegion(currSpeakerSet.tempSpeakerObjects[currRegIdx]); 1737 2467 } else { 1738 2468 console.log("no region selected"); … … 1742 2472 } 1743 2473 1744 function getTimeInSecondsFromInput(input) { // returns time in seconds from start or end input 2474 /** 2475 * Calculates time in seconds of start or end time input group 2476 * @param {element} input Element of time input groups: hh:mm:ss 2477 * @returns {int} Time in seconds 2478 */ 2479 function getTimeInSecondsFromInput(input) { 1745 2480 let hours = input.children[0].valueAsNumber; 1746 2481 let mins = input.children[1].valueAsNumber; … … 1749 2484 } 1750 2485 2486 /** 2487 * Sets the start or end time element group inputs 2488 * @param {element} input Element of time input group to be updated 2489 * @param {int} seconds Duration in seconds to be converted into hh:mm:ss 2490 */ 1751 2491 function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds 1752 2492 let date = new Date(null); … … 1759 2499 if (e.classList.contains("seconds") && !e.value.includes(".")) { e.value = e.value + ".0"; } 1760 2500 else if (e.value.length === 1){ e.value = '0' + e.value; }// 0 padded on left 1761 // if (e.value.length === 3) {e.value = '0' + e.value ; console.log('3: ' + e.value)} // 0 on the left (doesn't work on FF)1762 2501 }); 1763 2502 } 1764 2503 2504 /** 2505 * Adds a new undo state to the global undo state list 2506 * @param {object} state Primary set at current state 2507 * @param {object} secState Secondary set at current state 2508 * @param {boolean} isSec Whether or not current change was made to primary (false) or secondary (true) set 2509 * @param {boolean} dualMode Whether or not audio editor was in dual mode when undo state was added 2510 * @param {string} type Type of change e.g "remove", "speaker-change" 2511 * @param {int} currRegIdx Index of currently selected region (for restoration) 2512 * @param {Array} currRegIdxs Index of currently selected regions, if applicable (for restoration) 2513 */ 1765 2514 function addUndoState(state, secState, isSec, dualMode, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack 1766 2515 let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references 1767 2516 let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references 2517 let changedTrack = (type == "dualModeChange" || type == "selectAllChange") ? "none" : selectedVersions[isSec ? 1 : 0] // sets changedTrack to version name of edited region set 1768 2518 undoButton.classList.remove("disabled"); 1769 2519 undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made 1770 undoStates.push({state: newState, secState: newSecState, isSec: isSec, dualMode: dualMode, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});2520 undoStates.push({state: newState, secState: newSecState, isSec: isSec, changedTrack: changedTrack, dualMode: dualMode, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type}); 1771 2521 if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously 1772 2522 undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits) … … 1783 2533 } 1784 2534 1785 function undo() { // undo action: go back one state in the undoStates stack 2535 /** 2536 * Returns to the previous state in the undo state list 2537 */ 2538 function undo() { 1786 2539 if (!undoButton.classList.contains("disabled") && editMode) { // ensure there exist states to undo to 1787 2540 clearChapterSearch(); … … 1791 2544 let adjustedUndoLevel = undoLevel-1; 1792 2545 if (undoStates[undoLevel].type == "dualModeChange") { // toggle dual mode 1793 dualModeCheckbox.checked = !dualMode;1794 2546 dualModeChanged(true); 1795 2547 } else if (undoStates[undoLevel].type == "selectAllChange") { // toggle select all … … 1808 2560 else caretClicked("primary-caret"); 1809 2561 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state 1810 // console.log("undo-ing to index " + undoStates[undoLevel].currentRegionIndex);1811 2562 } else if (undoStates[undoLevel].currentRegionIndex) { 1812 2563 if (!dualMode) selectedSpeakerSet = primarySet; … … 1824 2575 } 1825 2576 editsMade = true; 1826 1827 2577 undoLevel--; // decrement undoLevel 1828 2578 reloadRegionsAndChapters(); … … 1835 2585 } 1836 2586 1837 function redo() { // redo action: go forward one state in the undoStates stack 2587 /** 2588 * Moves forward one state in the undo state list 2589 */ 2590 function redo() { 1838 2591 if (!redoButton.classList.contains("disabled") && editMode) { // ensure there exist states to redo to 1839 2592 clearChapterSearch(); … … 1841 2594 else { 1842 2595 if (undoStates[undoLevel+1].type == "dualModeChange") { // toggle dual mode 1843 dualModeCheckbox.checked = !dualMode;1844 2596 dualModeChanged(true); 1845 2597 } else if (undoStates[undoLevel+1].type == "selectAllChange") { // toggle select all … … 1850 2602 secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state 1851 2603 let selectedSpeakerSet; 1852 1853 2604 // handle currentRegion change 1854 2605 removeCurrentRegion(); … … 1865 2616 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex]; 1866 2617 } 1867 1868 // console.log("redo-ing to index " + undoStates[undoLevel+1].currentRegionIndex);1869 2618 if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) { 1870 2619 for (const idx of undoStates[undoLevel+1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]); … … 1873 2622 } 1874 2623 editsMade = true; 1875 1876 1877 2624 reloadRegionsAndChapters(); 1878 2625 undoLevel++; // increment undoLevel … … 1882 2629 } 1883 2630 if (undoLevel < undoStates.length) undoButton.classList.remove("disabled"); 1884 // console.log("new undoLevel: " + undoLevel);1885 2631 } 1886 2632 } 1887 2633 1888 2634 function resetUndoStates() { // clear undo history 1889 // console.log('resetUndoStates')1890 2635 undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}]; 1891 2636 undoLevel = 0; … … 1901 2646 drawCurrentRegionBounds(); 1902 2647 } 1903 } 1904 1905 function drawCurrentRegionBounds() { 2648 if (document.getElementById('new-canvas')) { document.getElementById('new-canvas').style.left = "-" + wave.scrollLeft + 'px' } // update placeholder waveform scroll position 2649 } 2650 2651 function drawCurrentRegionBounds() { // draws bounds of current region 1906 2652 removeRegionBounds(); 1907 if (editMode) { 1908 let currIndexes = getCurrentRegionsIndexes(); 1909 if (getCurrentRegionIndex != 0) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick"); 1910 for (let i = 0; i < currIndexes.length; i++) { 1911 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, wave.scrollLeft, "FireBrick"); 1912 } 1913 } 1914 } 1915 2653 let currIndexes = getCurrentRegionsIndexes(); 2654 if (getCurrentRegionIndex() != -1) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick"); 2655 for (let i = 0; i < currIndexes.length; i++) { 2656 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, wave.scrollLeft, "FireBrick"); 2657 } 2658 } 2659 2660 /** 2661 * Draws bounding 'n' above hovered or selected region 2662 * @param {object} region Region to have bound drawn for 2663 * @param {number} scrollPos Scroll position of div, used to offset draw position 2664 * @param {string} colour Colour to draw bound (black and FireBrick are used) 2665 */ 1916 2666 function drawRegionBounds(region, scrollPos, colour) { // draws on canvas to show bounds of hovered/selected region 1917 2667 const hoverSpeakerCanvas = document.createElement("canvas"); … … 1920 2670 hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds 1921 2671 const ctx = hoverSpeakerCanvas.getContext("2d"); 1922 1923 ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency 2672 // ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency 1924 2673 ctx.lineWidth = 1; 1925 2674 if (colour == "FireBrick") ctx.lineWidth = 3; … … 1943 2692 } 1944 2693 1945 function updateCurrSpeakerSet() { 2694 function updateCurrSpeakerSet() { // updates 'currSpeakerSet' var 1946 2695 if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet; 1947 2696 else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet; … … 1950 2699 function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions) 1951 2700 let output = []; 1952 for (let i = 0; i < inputArray.length; i++) { output.push({speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end }) } 2701 for (let i = 0; i < inputArray.length; i++) { 2702 output.push({ speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end, locked: (inputArray[i].locked === "true" || inputArray[i].locked === true) }); 2703 } 1953 2704 return output; 1954 2705 } 1955 2706 1956 function flashChapters() { 2707 function flashChapters() { // flashes chapters a lighter colour momentarily to indicate an update/change 1957 2708 chapters.style.backgroundColor = "rgb(66, 84, 88)"; 1958 2709 setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500); 1959 2710 } 1960 2711 1961 function fullscreenChanged() { // fullscreen onChange handler, increases waveform height & adjusts padding/margin 2712 /** Fullscreen onChange handler, increases waveform height & adjusts padding/margin */ 2713 function fullscreenChanged() { 1962 2714 if (!audioContainer.classList.contains("fullscreen")) { 1963 2715 audioContainer.classList.add("fullscreen"); 1964 wavesurfer.setHeight(175); 2716 wavesurfer.setHeight(175); // increase waveform height 2717 caretContainer.style.paddingLeft = "2rem"; 2718 caretContainer.style.height = wavesurfer.getHeight() + "px"; // set height to waveform height 2719 audioContainer.prepend(caretContainer); // attach to audioContainer (otherwise doesn't show due to AC being fullscreen) 1965 2720 } else { 1966 2721 audioContainer.classList.remove("fullscreen"); 1967 wavesurfer.setHeight(128); 1968 } 1969 } 1970 1971 function toggleFullscreen() { // toggles fullscreen mode of audio player/editor 2722 wavesurfer.setHeight(140); 2723 caretContainer.style.paddingLeft = "0"; 2724 caretContainer.style.height = wavesurfer.getHeight() + "px"; 2725 audioContainer.parentElement.prepend(caretContainer); // move back up in DOM hierarchy 2726 } 2727 setTimeout(() => { // ensures waveform shows 2728 zoomOutButton.click(); 2729 zoomInButton.click(); 2730 }, 250); 2731 } 2732 2733 /** Enables / disables the fullscreen view of audio player / editor */ 2734 function toggleFullscreen() { 1972 2735 if ((document.fullscreenElement && document.fullscreenElement !== null) || 1973 2736 (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) || … … 1976 2739 document.exitFullscreen(); 1977 2740 } else { 1978 audioContainer.requestFullscreen(); 2741 if (audioContainer.requestFullscreen) { 2742 audioContainer.requestFullscreen(); 2743 } else if (audioContainer.webkitRequestFullscreen) { /* Safari */ 2744 audioContainer.webkitRequestFullscreen(); 2745 } else if (audioContainer.msRequestFullscreen) { /* IE11 */ 2746 audioContainer.msRequestFullscreen(); 2747 } 1979 2748 } 1980 2749 } 1981 2750 } 1982 2751 2752 /** 2753 * Formats seconds to hh:mm:ss 2754 * @param {number} duration 2755 * @returns {string} Time in hh:mm:ss format 2756 */ 1983 2757 function formatAudioDuration(duration) { 1984 2758 // console.log('duration: ' + duration);
Note:
See TracChangeset
for help on using the changeset viewer.