Changeset 37031


Ignore:
Timestamp:
2022-12-15T15:43:21+13:00 (17 months ago)
Author:
davidb
Message:

added/improved diarization edit functionality in enriched audio player

Location:
main/trunk/greenstone3/web/interfaces/default
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js

    r36906 r37031  
    1111*/
    1212function urlEncodeChar(single_char_string) {
    13     /*var hex = Number(single_char_string.charCodeAt(0)).toString(16);
     13    /*let hex = Number(single_char_string.charCodeAt(0)).toString(16);
    1414    var str = "" + hex;
    1515    str = "%" + str.toUpperCase();
     
    292292
    293293function loadAudio(audio, sectionData) {
    294    var speakerObjects = [];
    295    var uniqueSpeakers;
     294   let editMode = false;
     295   let currentRegion = {speaker: '', start: '', end: ''};
     296   let currentRegions = [];
     297
     298   // let speakerObjects = [];
     299   // let tempSpeakerObjects = [];
     300   // let uniqueSpeakers;
    296301   const inputFile = sectionData;
    297    var itemType;
    298 
    299    // var accentColour = "#4CA72D";
    300    var accentColour = "#F8C537";
    301    var regionTransparency = "59";
    302 
    303    var waveformContainer = document.getElementById("waveform");
     302   let itemType;
     303
     304   let dualMode = false;
     305   let secondaryLoaded = false;
     306
     307   let editsMade = false;
     308   let undoLevel = 0;
     309   let undoStates = [];
     310   let prevUndoState = "";
     311   let tempZoomSave = 0;
     312   let isZooming;
     313
     314   let accentColour = "#66d640";
     315   // let accentColour = "#F8C537";
     316   let regionTransparency = "50";
     317
     318   let waveformContainer = document.getElementById("waveform");
    304319   
    305    var wavesurfer = WaveSurfer.create({ // wavesurfer options
     320   let wavesurfer = WaveSurfer.create({ // wavesurfer options
    306321      container: waveformContainer,
    307322      backend: "MediaElement",
    308       backgroundColor: "rgb(54, 73, 78)",
     323      backgroundColor: "rgb(40, 54, 58)",
     324      // backgroundColor: "rgb(24, 36, 39)",
    309325      waveColor: "white",
    310326      progressColor: accentColour,
    311       barWidth: 2,
     327      // progressColor: "grey",
     328      // barWidth: 2,
    312329      barHeight: 1.2,
    313       barGap: 2,
    314       barRadius: 1,
     330      // barGap: 2,
     331      // barRadius: 1,
    315332      cursorColor: 'black',
     333      cursorWidth: 2,
     334      normalize: true, // normalizes by maximum peak
    316335      plugins: [
    317          WaveSurfer.regions.create(),
     336         WaveSurfer.regions.create({
     337            // formatTimeCallback: function(a, b) {
     338            //    return "TEST";
     339            // }
     340         }),
    318341         WaveSurfer.timeline.create({
    319342            container: "#wave-timeline",
     
    329352                'background-color': '#000',
    330353                color: '#fff',
    331                 padding: '0.2em',
     354                padding: '0.25rem',
    332355                'font-size': '12px'
    333356            },
     
    341364   // wavesurfer events
    342365
    343    wavesurfer.on('region-click', function(region, e) { // play region audio on click
     366   wavesurfer.on('region-click', handleRegionClick);
     367
     368   function handleRegionClick(region, e) {
    344369      e.stopPropagation();
    345       handleRegionColours(region, true);
    346       wavesurfer.play(region.start); // plays from start of region
    347       // region.play(); // plays region only
     370      if (!editMode) { // play region audio on click
     371         wavesurfer.play(region.start); // plays from start of region
     372      } else { // select / deselect current region
     373         if (region.element.classList.contains("region-top")) caretClicked("primary-caret");
     374         else if (region.element.classList.contains("region-bottom")) caretClicked("secondary-caret");
     375         prevUndoState = "";
     376
     377         if (!e.ctrlKey && !e.shiftKey) {
     378            currentRegions = [];
     379            if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) {
     380               removeCurrentRegion(); // deselect current region on click
     381            } else {
     382               currentRegion = region;
     383               currentRegion.speaker = currentRegion.attributes.label;
     384               region.play(); // start and stop to move play cursor to beginning of region
     385               wavesurfer.playPause();
     386            }
     387         } else if (e.ctrlKey) { // control was held during click
     388            if (currentRegions.length == 0 && isCurrentRegion(region)) {
     389               removeCurrentRegion();
     390            } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) {
     391               const removeIndex = getIndexInCurrentRegions(region);
     392               if (removeIndex != -1) currentRegions.splice(removeIndex, 1);
     393               if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed
     394                  currentRegion = currentRegions[0];
     395                  // currentRegions = [];
     396               }
     397            } else {
     398               if (currentRegions.length < 1) currentRegions.push(currentRegion);
     399               if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist
     400               currentRegion = region;
     401               currentRegion.speaker = currentRegion.attributes.label;
     402               region.play();
     403               wavesurfer.playPause();
     404            }
     405            if (currentRegions.length == 1)  currentRegions = []; // clear selected regions if there is only one
     406         } else if (e.shiftKey) { // shift was held during click
     407            if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {
     408               if (currentRegions && currentRegions.length > 0) {
     409                  if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down
     410                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1);
     411                  } else { // shifting backwards / up
     412                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1);
     413                  }
     414               } else {
     415                  if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down
     416                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1);
     417                  } else { // shifting backwards / up
     418                     currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1);
     419                  }
     420               }
     421            }
     422         }
     423         if (speakerCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }
     424         reloadRegionsAndChapters();
     425      }
     426   }
     427
     428   function getIndexInCurrentRegions(region) {
     429      for (const reg of currentRegions) {
     430         const regSpeaker = reg.attributes ? reg.attributes.label : reg.speaker;
     431         if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label) {
     432            return currentRegions.indexOf(reg);
     433         }
     434      }
     435      return -1;
     436   }
     437
     438   function getIndexOfRegion(region) {
     439      for (const reg of currSpeakerSet.tempSpeakerObjects) {
     440         if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label) {
     441            return currSpeakerSet.tempSpeakerObjects.indexOf(reg);
     442         }
     443      }
     444      return -1;
     445   }
     446
     447   wavesurfer.on('region-mouseenter', function(region) { // region hover effects
     448      handleRegionColours(region, true);
     449      hoverSpeaker.innerHTML = region.attributes.label; 
     450      hoverSpeaker.style.marginLeft = parseInt(region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     451      if (!isInCurrentRegions(region)) {
     452         removeRegionBounds();
     453         drawRegionBounds(region, waveform.scrollLeft, "black");
     454      }
     455      if (isCurrentRegion(region)) drawRegionBounds(region, waveform.scrollLeft);
    348456   });
    349 
    350    wavesurfer.on('region-mouseenter', function(region) { handleRegionColours(region, true); });
    351    wavesurfer.on('region-mouseleave', function(region) { if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); });
     457   wavesurfer.on('region-mouseleave', function(region) {
     458      if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false);
     459      removeRegionBounds();
     460      if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
     461         hoverSpeaker.innerHTML = currentRegion.speaker;
     462         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     463         let currIndexes = getCurrentRegionsIndexes();
     464         for (let i = 0; i < currIndexes.length; i++) {
     465            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
     466         }
     467         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
     468      }
     469      if (!currentRegion.speaker) hoverSpeaker.innerHTML = "";
     470   });
    352471   wavesurfer.on('region-in', function(region) {
    353472      handleRegionColours(region, true);
     
    359478      }
    360479   });
    361    wavesurfer.on('region-out', function(region) { handleRegionColours(region, false); });
    362 
    363    var loader = document.createElement("span"); // loading audio element
     480   wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });
     481   wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event
     482   // wavesurfer.on('region-update-end', (region, e) => { handleRegionEdit(region, e)} ); // end of click-drag event
     483
     484   let loader = document.createElement("span"); // loading audio element
    364485   loader.innerHTML = "Loading audio";
    365486   loader.id = "waveform-loader";
     
    369490      if (inputFile.endsWith("csv")) { // diarization if csv
    370491         itemType = "chapter";
    371          loadCSVFile(inputFile, ["speaker", "start", "end"]);
     492         loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);
    372493      } else if (inputFile.endsWith("json")) { // transcription if json
    373494         itemType = "word";
     
    377498      }
    378499      loader.remove(); // remove load text
     500      chapters.style.cursor = "pointer";
     501      waveform.className = "audio-scroll";
    379502   });
    380503   
    381504   function downloadURI(loc, name) {
    382       var link = document.createElement("a");
     505      let link = document.createElement("a");
    383506      link.download = name;
    384507      link.href = loc;
     
    387510
    388511   // toolbar elements & event handlers
    389    var chapters = document.getElementById("chapters");
    390    var chapterButton = document.getElementById("chapterButton");
    391    var backButton = document.getElementById("backButton");
    392    var playPauseButton = document.getElementById("playPauseButton");
    393    var forwardButton = document.getElementById("forwardButton");
    394    var downloadButton = document.getElementById("downloadButton");
    395    var muteButton = document.getElementById("muteButton");
    396    var zoomSlider = document.getElementById("slider");
     512   const audioContainer = document.getElementById("audioContainer");
     513   const dualModeCheckbox = document.getElementById("dual-mode-checkbox");
     514   const waveform = document.getElementsByTagName("wave")[0];
     515   const primaryCaret = document.getElementById("primary-caret");
     516   const secondaryCaret = document.getElementById("secondary-caret");
     517   const chapters = document.getElementById("chapters");
     518   const editPanel = document.getElementById("edit-panel");
     519   const chapterButton = document.getElementById("chapterButton");
     520   const zoomOutButton = document.getElementById("zoomOutButton");
     521   const zoomSlider = document.getElementById("zoom-slider");
     522   const zoomInButton = document.getElementById("zoomInButton");
     523   const backButton = document.getElementById("backButton");
     524   const playPauseButton = document.getElementById("playPauseButton");
     525   const forwardButton = document.getElementById("forwardButton");
     526   const editButton = document.getElementById("editButton");
     527   const downloadButton = document.getElementById("downloadButton");
     528   const muteButton = document.getElementById("muteButton");
     529   const volumeSlider = document.getElementById("volume-slider");
     530   const fullscreenButton = document.getElementById("fullscreenButton");
     531   const speakerCheckbox = document.getElementById("change-all-checkbox");
     532   const changeAllLabel = document.getElementById("change-all-label");
     533   const speakerInput = document.getElementById("speaker-input");
     534   const startTimeInput = document.getElementById("start-time-input");
     535   const endTimeInput = document.getElementById("end-time-input");
     536   const removeButton = document.getElementById("remove-button");
     537   const createButton = document.getElementById("create-button");
     538   const discardButton = document.getElementById("discard-button");
     539   const undoButton = document.getElementById("undo-button");
     540   const redoButton = document.getElementById("redo-button");
     541   const saveButton = document.getElementById("save-button");
     542   const hoverSpeaker = document.getElementById("hover-speaker");
     543
     544   audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() });
     545   dualModeCheckbox.addEventListener("change", dualModeChanged);
     546   waveform.addEventListener('scroll', (e) => { waveformScrolled() })
     547   primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
     548   secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
    397549   chapters.style.height = "0px";
    398    chapterButton.addEventListener("click", function() { toggleChapters(); });
    399    backButton.addEventListener("click", function() { wavesurfer.skipBackward(); });
    400    playPauseButton.addEventListener("click", function() { wavesurfer.playPause() });
    401    forwardButton.addEventListener("click", function() { wavesurfer.skipForward(); });
    402    // audio = /greenstone3/library/sites/localsite/collect/tiriana-audio/index/assoc/HASHa6b7.dir/Te_Kakano_CH09_B.mp3
    403    downloadButton.addEventListener("click", function() { downloadURI(audio, audio.split(".dir/")[1]) });
    404    muteButton.addEventListener("click", function() { wavesurfer.toggleMute(); });
     550   editPanel.style.height = "0px";
     551   chapterButton.addEventListener("click", () => { toggleChapters() });
     552   zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) });
     553   zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) });
     554   backButton.addEventListener("click", () => { wavesurfer.skipBackward(); });
     555   playPauseButton.addEventListener("click", () => { wavesurfer.playPause() });
     556   forwardButton.addEventListener("click", () => { wavesurfer.skipForward(); });
     557   editButton.addEventListener("click", toggleEditMode);
     558   downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) });
     559   muteButton.addEventListener("click", () => { wavesurfer.toggleMute() });
     560   volumeSlider.style["accent-color"] = accentColour;
     561   fullscreenButton.addEventListener("click", toggleFullscreen);
    405562   zoomSlider.style["accent-color"] = accentColour;
     563   speakerCheckbox.addEventListener("change", speakerCheckboxChanged);
     564   speakerInput.addEventListener("input", speakerChange);
     565   speakerInput.addEventListener("blur", speakerInputUnfocused);
     566   createButton.addEventListener("click", createNewRegion);
     567   removeButton.addEventListener("click", removeRegion);
     568   discardButton.addEventListener("click", discardRegionChanges);
     569   undoButton.addEventListener("click", undo);
     570   redoButton.addEventListener("click", redo);
     571   saveButton.addEventListener("click", saveRegionChanges);
     572   document.querySelectorAll('input[type=number]').forEach(e => {
     573      e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed
     574      e.onblur = () => { prevUndoState = "" };
     575   });
     576   audioContainer.addEventListener("keyup", keyPressed);
     577
     578   function keyPressed(e) {
     579      if (e.target.tagName !== "INPUT") {
     580         if (e.code === "Backspace" || e.code === "Delete") removeRegion();
     581         else if (e.code === "Space") wavesurfer.playPause();
     582         else if (e.code === "ArrowLeft") wavesurfer.skipBackward();
     583         else if (e.code === "ArrowRight") wavesurfer.skipForward();
     584      }
     585   }
     586
     587   function dualModeChanged(e) { // on dualmode checkbox value change
     588      dualMode = e.target.checked;
     589      currSpeakerSet = primarySet;
     590      reloadRegionsAndChapters();
     591      if (dualMode) {
     592         if (!secondaryLoaded) {
     593            loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet);
     594            secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once
     595         }
     596         document.getElementById("caret-container").style.display = "flex";
     597      } else {
     598         document.getElementById("caret-container").style.display = "none";
     599      }
     600      currSpeakerSet = primarySet;
     601   }
    406602
    407603   // path to toolbar images
    408    var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
    409 
    410    wavesurfer.on("play", function() { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
    411    wavesurfer.on("pause", function() { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
     604   let interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
     605
     606   function caretClicked(id) {
     607      wavesurfer.clearRegions();
     608      // flashChapters();
     609      if (id === "primary-caret") {
     610         currSpeakerSet = primarySet;
     611         swapCarets(true);
     612      } else if (id === "secondary-caret") {
     613         currSpeakerSet = secondarySet;
     614         swapCarets(false);
     615      }
     616      $(".region-top").remove();
     617      $(".region-bottom").remove();
     618      populateChapters(primarySet);
     619      populateChapters(secondarySet);
     620   }
     621
     622   function swapCarets(toPrimary) {
     623      const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false;
     624      if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
     625      if (toPrimary) {
     626         primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
     627         secondaryCaret.src = interface_bootstrap_images + "caret-right.svg";
     628      } else {
     629         primaryCaret.src = interface_bootstrap_images + "caret-right.svg";
     630         secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
     631      }
     632   }
     633
     634   wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
     635   wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
    412636   wavesurfer.on("mute", function(mute) {
    413637      if (mute) {
    414638         muteButton.src = interface_bootstrap_images + "mute.svg";
    415639         muteButton.style.opacity = 0.6;
     640         volumeSlider.value = 0;
    416641      }
    417642      else {
    418643         muteButton.src = interface_bootstrap_images + "unmute.svg";
    419644         muteButton.style.opacity = 1;
     645         volumeSlider.value = 1;
    420646      }
    421647   });
    422648
    423    zoomSlider.oninput = function () { // slider changes waveform zoom
     649   volumeSlider.addEventListener("input", function() {
     650      wavesurfer.setVolume(this.value);
     651      if (this.value == 0) {
     652         muteButton.src = interface_bootstrap_images + "mute.svg";
     653         muteButton.style.opacity = 0.6;
     654      } else {
     655         muteButton.src = interface_bootstrap_images + "unmute.svg";
     656         muteButton.style.opacity = 1;
     657      }
     658   });
     659
     660   zoomSlider.addEventListener("input", function() { // slider changes waveform zoom
    424661      wavesurfer.zoom(Number(this.value) / 4);
    425    };
     662      if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
     663         hoverSpeaker.innerHTML = currentRegion.speaker;
     664         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     665         removeRegionBounds();
     666         let currIndexes = getCurrentRegionsIndexes();
     667         for (let i = 0; i < currIndexes.length; i++) {
     668            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
     669         }
     670         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
     671      }
     672      let handles = document.getElementsByClassName("wavesurfer-handle");
     673      if (this.value < 20) {
     674         for (const handle of handles) {
     675            handle.style.setProperty("width", "1px", "important");
     676         }
     677      } else {
     678         for (const handle of handles) {
     679            handle.style.setProperty("width", "3px", "important");
     680         }
     681      }
     682   });
    426683   wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point
    427684
    428    var toggleChapters = function() { // show & hide chapter section
     685   let toggleChapters = function() { // show & hide chapter section
    429686      if (chapters.style.height == "0px") {
    430687         chapters.style.height = "30vh";
     
    434691   }
    435692
    436    function loadCSVFile(filename, manualHeader) { // based around: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
     693   function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) {
     694      this.isSecondary = isSecondary;
     695      this.uniqueSpeakers = uniqueSpeakers;
     696      this.speakerObjects = speakerObjects;
     697      this.tempSpeakerObjects = tempSpeakerObjects;
     698   }
     699   let primarySet = new SpeakerSet(false, [], [], []);
     700   let secondarySet = new SpeakerSet(true, [], [], []);
     701   let currSpeakerSet = primarySet;
     702
     703   function loadCSVFile(filename, manualHeader, speakerSet) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
     704      // if (speakerSet) currSpeakerSet = speakerSet; // if parameter is given, set
    437705      $.ajax({
    438706         type: "GET",
     
    440708         dataType: "text",
    441709      }).then(function(data) {
    442          var dataLines = data.split(/\r\n|\n/);
    443          var headers;
    444          var startIndex;
    445          uniqueSpeakers = []; // used for obtaining unique colours
    446          speakerObjects = []; // list of speaker items
     710         let dataLines = data.split(/\r\n|\n/);
     711         let headers;
     712         let startIndex;
     713         speakerSet.uniqueSpeakers = []; // used for obtaining unique colours
     714         speakerSet.speakerObjects = []; // list of speaker items
    447715
    448716         if (manualHeader) { // headers for columns can be provided if not existent in csv
     
    454722         }
    455723
    456          for (var i = startIndex; i < dataLines.length; i++) {
    457             var data = dataLines[i].split(',');
     724         for (let i = startIndex; i < dataLines.length; i++) {
     725            let data = dataLines[i].split(',');
    458726            if (data.length == headers.length) {
    459                var item = {};
    460                for (var j = 0; j < headers.length; j++) {
     727               let item = {};
     728               for (let j = 0; j < headers.length; j++) {
    461729                  item[headers[j]] = data[j];
    462                   if (j == 0) {
    463                      if (!uniqueSpeakers.includes(data[j])) {
    464                         uniqueSpeakers.push(data[j]);
    465                      }
     730                  if (j == 0 && !speakerSet.uniqueSpeakers.includes(data[j])) {
     731                     speakerSet.uniqueSpeakers.push(data[j]);
    466732                  }
    467733               }
    468                speakerObjects.push(item);
     734               speakerSet.speakerObjects.push(item);
    469735            }
    470736         }
    471          populateChapters(speakerObjects);
     737         speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects);
     738         populateChapters(speakerSet);
     739         resetUndoStates(); // undo stack init
    472740      });
    473741   }
    474742
    475743   function populateChapters(data) { // populates chapter section and adds regions to waveform
    476       // colorbrewer is a web tool for guidance in choosing map colour schemes based on a variety of settings.
     744      // colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings.
    477745      // this colour scheme is designed for qualitative data
    478746
    479       if (uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8];
    480       else if (uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3];
    481       else  colourbrewerset = colorbrewer.Set2[uniqueSpeakers.length];
    482 
    483       for (var i = 0; i < data.length; i++) {
    484          var chapter = document.createElement("div");
    485          var speakerLetter = getLetter(data[i]);
     747      if (data.uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8];
     748      else if (data.uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3];
     749      else  colourbrewerset = colorbrewer.Set2[data.uniqueSpeakers.length];
     750
     751      let dataIsSelected = false;
     752
     753      if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) dataIsSelected = true;
     754      if (dataIsSelected || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population
     755      data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time
     756
     757      for (let i = 0; i < data.tempSpeakerObjects.length; i++) {
     758         let chapter = document.createElement("div");
     759         chapter.classList.add("chapter");
    486760         chapter.id = "chapter" + i;
    487          chapter.classList.add("chapter");
    488          chapter.innerHTML = "Speaker " + speakerLetter + "<span class='speakerTime' id='" + "chapter" + i + "'>" + minutize(data[i].start) + " - " + minutize(data[i].end) + "</span>";
    489          chapter.addEventListener("click", e => { chapterClicked(e.target.id) });
    490          chapter.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
    491          chapter.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
    492          chapters.appendChild(chapter);
    493          // console.log("index: " + uniqueSpeakers.indexOf(data[i].speaker)%8);
    494          // console.log("colour: " + colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8]);
    495          wavesurfer.addRegion({
     761         let speakerName = data.tempSpeakerObjects[i].speaker;
     762         let speakerTime = document.createElement("span");
     763         speakerTime.classList.add("speakerTime");
     764         speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s";
     765         chapter.innerHTML = speakerName;
     766         chapter.appendChild(speakerTime);
     767         chapter.addEventListener("click", chapterClicked);
     768         chapter.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     769         chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     770
     771         let selected = false;
     772         let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end };
     773
     774         if ((dataIsSelected || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
     775            chapter.classList.add("selected-chapter");
     776            selected = true;
     777         }
     778
     779         if (dataIsSelected || !dualMode) chapters.appendChild(chapter);
     780
     781         let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region
    496782            id: "region" + i,
    497             start: data[i].start,
    498             end: data[i].end,
    499             drag: false,
    500             resize: false,
    501             color: colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8] + regionTransparency,
     783            start: data.tempSpeakerObjects[i].start,
     784            end: data.tempSpeakerObjects[i].end,
     785            drag: editMode,
     786            resize: editMode,
     787            attributes: {
     788               label: speakerName,
     789            },
     790            color: colourbrewerset[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,
     791            ...(selected) && {color: "rgba(255,50,50,0.5)"},
    502792         });
    503       }
     793         data.tempSpeakerObjects[i].region = associatedReg;
     794      }
     795
     796      let regions = document.getElementsByTagName("region");
     797      if (dualMode) {
     798         if (document.getElementsByClassName("region-top").length === 0) for (const reg of regions) reg.classList.add("region-top");
     799         else for (const rego of regions) if (!rego.classList.contains("region-top")) rego.classList.add("region-bottom");
     800      }
     801      if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important");
     802      else for (const reg of regions) reg.style.setProperty("z-index", "1", "important");
    504803   }
    505804
     
    513812
    514813   function populateWords(data) { // populates word section and adds regions to waveform
    515       var transcription = data.transcription;
    516       var words = data.words;
    517       var wordContainer = document.createElement("div");
     814      let transcription = data.transcription;
     815      let words = data.words;
     816      let wordContainer = document.createElement("div");
    518817      wordContainer.id = "word-container";
    519       for (var i = 0; i < words.length; i++) {
    520          var word = document.createElement("span");
     818      for (let i = 0; i < words.length; i++) {
     819         let word = document.createElement("span");
    521820         word.id = "word" + i;
    522821         word.classList.add("word");
    523822         word.innerHTML = transcription.split(" ")[i];
    524823         word.addEventListener("click", e => { wordClicked(data, e.target.id) });
    525          word.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
    526          word.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
     824         word.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     825         word.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
    527826         wordContainer.appendChild(word);
    528827         wavesurfer.addRegion({
     
    538837   }
    539838
    540    var chapterClicked = function(id) { // plays audio from start of chapter
    541       var index = id.replace("chapter", "");
    542       var start = speakerObjects[index].start;
    543       var end = speakerObjects[index].end;
    544       // wavesurfer.play(start, end);
     839   let chapterClicked = function(e) { // plays audio from start of chapter
     840      let index = e.target.id.replace("chapter", "");
     841      let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region;
     842      handleRegionClick(clickedRegion, e);
     843   }
     844
     845   function wordClicked(data, id) { // plays audio from start of word
     846      let index = id.replace("word", "");
     847      let start = data.words[index].startTime;
    545848      wavesurfer.play(start);
    546849   }
    547850
    548    function wordClicked(data, id) { // plays audio from start of word
    549       var index = id.replace("word", "");
    550       var start = data.words[index].startTime;
    551       var end = data.words[index].endTime;
    552       // wavesurfer.play(start, end);
    553       wavesurfer.play(start);
    554    }
    555 
    556    function chapterEnter(id) {
    557       regionEnter(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
    558    }
    559 
    560    function chapterLeave(id) {
    561       regionLeave(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
     851   function chapterEnter(idx) {
     852      let reg = currSpeakerSet.tempSpeakerObjects[idx].region;
     853      regionEnter(reg);
     854      hoverSpeaker.innerHTML = reg.attributes.label; 
     855      hoverSpeaker.style.marginLeft = parseInt(reg.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     856      if (!isInCurrentRegions(reg)) {
     857         removeRegionBounds();
     858         drawRegionBounds(reg, waveform.scrollLeft, "black");
     859      }
     860   }
     861
     862   function chapterLeave(idx) {
     863      regionLeave(currSpeakerSet.tempSpeakerObjects[idx].region);
     864      removeRegionBounds();
     865      hoverSpeaker.innerHTML = "";
     866      if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
     867         hoverSpeaker.innerHTML = currentRegion.speaker;
     868         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     869         let currIndexes = getCurrentRegionsIndexes();
     870         for (let i = 0; i < currIndexes.length; i++) {
     871            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
     872         }
     873         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
     874      }
    562875   }
    563876
    564877   function handleRegionColours(region, highlight) { // handles region, chapter & word colours
    565       var colour;
    566       if (highlight) {
    567          colour = "rgb(101, 116, 116)";
    568          regionEnter(region);
     878      if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) {
     879         let colour;
     880         if (highlight) {
     881            colour = "rgb(101, 116, 116)";
     882            regionEnter(region);
     883         } else {
     884            colour = "";
     885            regionLeave(region);
     886         }
     887         if (isCurrentRegion(region) || isInCurrentRegions(region)) {
     888            colour = "rgba(255, 50, 50, 0.5)";
     889         }
     890         let regionIndex = region.id.replace("region","");
     891         let corrItem = document.getElementById(itemType + regionIndex);
     892         corrItem.style.backgroundColor = colour; // updates chapter background (not region)
     893      }
     894   }
     895
     896   function regionEnter(region) {
     897      if (isCurrentRegion(region) || isInCurrentRegions(region)) {
     898         if (region.element.classList.contains("region-top") && !currSpeakerSet.isSecondary) region.update({ color: "rgba(255, 50, 50, 0.5)" });
    569899      } else {
    570          colour = "";
    571          regionLeave(region);
    572       }
    573       var regionIndex = region.id.replace("region","");
    574       var corrItem = document.getElementById(itemType + regionIndex);
    575       corrItem.style.backgroundColor = colour;
    576    }
    577 
    578    function regionEnter(region) {
    579       region.update({ color: "rgba(255, 255, 255, 0.35)" });
     900         region.update({ color: "rgba(255, 255, 255, 0.35)" });
     901      }
    580902   }
    581903
    582904   function regionLeave(region) {
    583905      if (itemType == "chapter") {
    584          if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
    585             var index = region.id.replace("region", "");
    586             region.update({ color: colourbrewerset[uniqueSpeakers.indexOf(speakerObjects[index].speaker)%8] + regionTransparency });
     906         if (isCurrentRegion(region) || isInCurrentRegions(region)) {
     907            region.update({ color: "rgba(255, 50, 50, 0.5)" });
     908         } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
     909            let index = region.id.replace("region", "");
     910            region.update({ color: colourbrewerset[currSpeakerSet.uniqueSpeakers.indexOf(currSpeakerSet.tempSpeakerObjects[index].speaker)%8] + regionTransparency });
    587911         }
    588912      } else {
     
    592916
    593917   function minutize(num) { // converts seconds to m:ss for chapters & waveform hover
    594       var seconds = Math.round(num % 60);
    595       if (seconds.toString().length == 1) seconds = "0" + seconds;
    596       return Math.floor(num / 60) + ":" + seconds;
     918      // return (num - (num %= 60)) / 60 + (9 < num ? ':' : ':0') + ~~num; // https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
     919
     920      let date = new Date(null);
     921      date.setSeconds(num);
     922      return date.toTimeString().split(" ")[0].substring(3);
    597923   }
    598924
    599925   function getLetter(val) {
    600       var speakerNum = parseInt(val.speaker.replace("SPEAKER_",""));
     926      // return val.replace("SPEAKER_","");
     927      let speakerNum = parseInt(val.replace("SPEAKER_",""));
    601928      return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65
    602929   }
     930
     931
     932
     933   // edit functionality
     934
     935   function toggleEditMode() { // toggles edit panel and redraws regions with resize handles
     936      toggleEditPanel();
     937      updateRegionEditPanel();
     938   }
     939
     940   function toggleEditPanel() { // show & hide edit panel
     941      currentRegion.speaker = '';
     942      currentRegion.start = '';
     943      currentRegion.end = '';
     944      currentRegions = [];
     945      removeRegionBounds();
     946      hoverSpeaker.innerHTML = "";
     947      if (editPanel.style.height == "0px") {
     948         if (chapters.style.height == "0px") chapters.style.height = "30vh"; // expands chapter panel
     949         editPanel.style.height = "30vh";
     950         editPanel.style.padding = "1rem";
     951         setRegionEditMode(true);
     952      } else {
     953         editPanel.style.height = "0px";
     954         editPanel.style.padding = "0px";
     955         setRegionEditMode(false);
     956      }
     957   }
     958
     959   function setRegionEditMode(state) {
     960      editMode = state;
     961      chapters.innerHTML = '';
     962      wavesurfer.clearRegions();
     963      populateChapters(currSpeakerSet);
     964   }
     965
     966   function handleRegionEdit(region, e) {
     967      if (e.target.localName === "region" || e.target.localName === "handle") {
     968         if (region.element.classList.contains("region-top")) { currSpeakerSet = primarySet; swapCarets(true) }
     969         else if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) }
     970         editsMade = true;
     971         currentRegion = region;
     972         region.play();
     973         wavesurfer.pause();
     974         let regionIndex = getCurrentRegionIndex();
     975         currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker;
     976         currSpeakerSet.tempSpeakerObjects[regionIndex].region = region;
     977         currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start;
     978         currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end;
     979         reloadRegionsAndChapters();
     980         handleSameSpeakerOverlap(getCurrentRegionIndex()); // recalculate index in case start pos has changed
     981         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "dragdrop", getCurrentRegionIndex());
     982         editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!)
     983      } else console.log("resizing too fast, selected region not updated.");
     984   }
     985
     986   function handleSameSpeakerOverlap(regionIdx) { // consumes/merges same-speaker regions with overlapping bounds
     987      let draggedRegion = currSpeakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here
     988      let draggedRegionSpeaker = draggedRegion.speaker;
     989      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     990         if (currSpeakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, currSpeakerSet.tempSpeakerObjects[i])) { // ensure speaker name match
     991            if (currSpeakerSet.tempSpeakerObjects[i].start < draggedRegion.end && draggedRegion.start < currSpeakerSet.tempSpeakerObjects[i].end) { // ensure overlap
     992               draggedRegion.start = Math.min(currSpeakerSet.tempSpeakerObjects[i].start, draggedRegion.start);
     993               draggedRegion.end = Math.max(currSpeakerSet.tempSpeakerObjects[i].end, draggedRegion.end);
     994               currentRegion = draggedRegion;
     995               currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region
     996               i = -1; // reset for loop to support multiple consumptions
     997            }
     998         }
     999      }
     1000      reloadRegionsAndChapters();
     1001   }
     1002
     1003   function updateRegionEditPanel() { // updates edit panel content/inputs
     1004      if (currentRegion && currentRegion.speaker == "") {
     1005         removeButton.classList.add("disabled");
     1006         speakerInput.classList.add("disabled");
     1007         speakerCheckbox.classList.add("disabled");
     1008         speakerCheckbox.disabled = true;
     1009         disableStartEndInputs();
     1010         speakerInput.readOnly = true;
     1011         speakerInput.value = "";
     1012      } else {
     1013         removeButton.classList.remove("disabled");
     1014         speakerInput.classList.remove("disabled");
     1015         speakerCheckbox.classList.remove("disabled");
     1016         if (!isZooming) speakerCheckbox.disabled = false;
     1017         enableStartEndInputs();
     1018         speakerInput.readOnly = false;
     1019      }
     1020      if (editsMade) {
     1021         discardButton.classList.remove("disabled");
     1022         saveButton.classList.remove("disabled");
     1023      } else {
     1024         discardButton.classList.add("disabled");
     1025         saveButton.classList.add("disabled");
     1026      }
     1027      if (speakerCheckbox.checked) {
     1028         // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")";
     1029         disableStartEndInputs();
     1030      }
     1031      if (currentRegion && currentRegion.speaker != "") {
     1032         speakerInput.value = currentRegion.speaker;
     1033         setInputInSeconds(startTimeInput, currentRegion.start);
     1034         setInputInSeconds(endTimeInput, currentRegion.end);
     1035      }
     1036   }
     1037
     1038   function createNewRegion() { // adds a new region to the waveform
     1039      const speaker = "NEW_SPEAKER"; // default name
     1040      let offset = 0;
     1041      if (!currSpeakerSet.uniqueSpeakers.includes(speaker)) { currSpeakerSet.uniqueSpeakers.push(speaker) }
     1042      else { offset = 5 * getRegionsWithSpeaker(speaker).length } // offset new region if multiple new regions are created. TODO: check region has different start time
     1043      const start = offset + wavesurfer.getCurrentTime();
     1044      const end = offset + wavesurfer.getCurrentTime() + 15;
     1045      currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end});
     1046      editsMade = true;
     1047      currentRegions = [];
     1048      currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end});
     1049      reloadRegionsAndChapters();
     1050      addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "create", getCurrentRegionIndex());
     1051   }
     1052
     1053   function getRegionFromProps(props) { // find region using speaker, start & end time
     1054      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     1055         if (currSpeakerSet.tempSpeakerObjects[i].speaker === props.speaker && currSpeakerSet.tempSpeakerObjects[i].start === props.start && currSpeakerSet.tempSpeakerObjects[i].end === props.end) {
     1056            return currSpeakerSet.tempSpeakerObjects[i];
     1057         }
     1058      }
     1059      console.log("getRegionFromProps failed to find matching region");
     1060   }
     1061
     1062   function removeRegion() { // removes currently selected region or regions
     1063      if (!removeButton.classList.contains("disabled")) {
     1064         if (getCurrentRegionIndex() != -1) { // if currentRegion has been set
     1065            let currentRegionIndex = getCurrentRegionIndex();
     1066            let currentRegionIndexes = getCurrentRegionsIndexes();
     1067            for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     1068               if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) {
     1069                  // if (!currentRegion.region) currentRegion.remove(); // remove from wavesurfer.regions.list
     1070                  currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects
     1071                  // else currentRegion.region.remove(); // remove if region was just added
     1072                  editsMade = true;
     1073                  if (i >= 0) i--; // decrement index for side-by-side regions
     1074                  if (!speakerCheckbox.checked && currentRegions.length < 1) {
     1075                     removeCurrentRegion();
     1076                     reloadRegionsAndChapters();
     1077                     addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex);
     1078                     return; // jump out of for loop
     1079                  }
     1080               } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) {
     1081                  currSpeakerSet.tempSpeakerObjects.splice(i, 1);
     1082                  if (i >= 0) i--;
     1083               }
     1084            }
     1085            removeCurrentRegion();
     1086            reloadRegionsAndChapters();
     1087            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
     1088         } else { console.log("no region selected") }
     1089      }
     1090   }
     1091
     1092   function regionsMatch(reg1, reg2) {
     1093      if (reg1.start == reg2.start && reg1.end == reg2.end) return true;
     1094      return false;
     1095   }
     1096
     1097   function isCurrentRegion(region) {
     1098      if (regionsMatch(currentRegion, region)) return true;
     1099      return false;
     1100   }
     1101
     1102   function isInCurrentRegions(region) {
     1103      if (currentRegions != []) {
     1104         for (let i = 0; i < currentRegions.length; i++) {
     1105            if (currentRegions[i].start == region.start && currentRegions[i].end == region.end) {
     1106               return true;
     1107            }
     1108         }
     1109      }
     1110      return false;
     1111   }
     1112
     1113   function getCurrentRegionIndex() { // returns the index of currently selected region
     1114      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     1115         if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i }
     1116      }
     1117      // if (dualMode) {
     1118      //    for (let i = 0; i < secondarySet.tempSpeakerObjects.length; i++) {
     1119      //       if (isCurrentRegion(secondarySet.tempSpeakerObjects[i].region)) { return i }
     1120      //    }
     1121      // }
     1122      return -1;
     1123   }
     1124
     1125   function getCurrentRegionsIndexes() { // returns the indexes of currently selected regions
     1126      let indexes = [];
     1127      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     1128         if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i].region)) { indexes.push(i) }
     1129      }
     1130      return indexes;
     1131   }
     1132
     1133   function removeCurrentRegion() { // removes current region, regions and bound markers
     1134      currentRegion = {speaker: '', start: '', end: ''};
     1135      currentRegions = [];
     1136      removeRegionBounds();
     1137      hoverSpeaker.innerHTML = "";
     1138   }
     1139
     1140   function getRegionsWithSpeaker(speaker) { // returns all regions with the given speaker name
     1141      let out = [];
     1142      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     1143         if (currSpeakerSet.tempSpeakerObjects[i].speaker === speaker) { out.push(currSpeakerSet.tempSpeakerObjects[i]) }
     1144      }
     1145      return out;
     1146   }
     1147
     1148   function sortSpeakerObjectsByStart(speakerOb) { // sorts the speaker object array by start time
     1149      return speakerOb.sort(function(a,b) {
     1150         return a.start - b.start;
     1151      });
     1152   }
     1153
     1154   function speakerChange() { // speaker input name onInput handler
     1155      const newSpeaker = speakerInput.value;
     1156      if (newSpeaker && newSpeaker != "") {
     1157         speakerInput.style.border = "2px solid transparent";
     1158         if (getCurrentRegionIndex() != -1) { // if a region is selected
     1159            if (!currSpeakerSet.uniqueSpeakers.includes(newSpeaker)) { currSpeakerSet.uniqueSpeakers.push(newSpeaker) }
     1160            if (currentRegions && currentRegions.length < 1) { currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker } // single change
     1161            else if (currentRegions && currentRegions.length > 1) { // multiple changes
     1162               for (idx of getCurrentRegionsIndexes()) currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker;
     1163            }
     1164            speakerInput.value = "";
     1165            currentRegion.speaker = newSpeaker;
     1166            editsMade = true;
     1167            reloadRegionsAndChapters();
     1168            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
     1169         } else { console.log("no region selected") }
     1170      } else { console.log("no text in speaker input"); speakerInput.style.border = "2px solid firebrick"; }
     1171   }
     1172
     1173   function speakerInputUnfocused() {
     1174      prevUndoState = "";
     1175      if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) {
     1176         speakerInput.style.border = "2px solid firebrick";
     1177         window.alert("Speaker input cannot be left empty. Please enter a speaker name.");
     1178         setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected
     1179      } else speakerInput.style.border = "2px transparent";
     1180   }
     1181
     1182   function speakerCheckboxChanged() { // "Change all" toggled
     1183      if (speakerCheckbox.checked) {
     1184         if (!isZooming) {
     1185            tempZoomSave = zoomSlider.value;
     1186            zoomTo(0); // zoom out to encompass all selected regions
     1187         }
     1188         let uniqueSelectedSpeakers;
     1189         if (currentRegions && currentRegions.length > 0) { // if more than one region selected
     1190            uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
     1191            uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
     1192         } else uniqueSelectedSpeakers = [currentRegion.speaker];
     1193         currentRegions = [];
     1194         for (const speaker of uniqueSelectedSpeakers) {
     1195            for (const region of getRegionsWithSpeaker(speaker)) currentRegions.push(region);
     1196         }
     1197         reloadRegionsAndChapters();
     1198      } else {
     1199         if (!isZooming) {
     1200            zoomTo(tempZoomSave / 4);  // zoom back in to previous level
     1201         }
     1202         currentRegions = []; // this will lose track of previously selected region*s*
     1203         // changeAllLabel.innerHTML = "Change all";
     1204         reloadRegionsAndChapters();
     1205      }
     1206   }
     1207
     1208   function enableStartEndInputs() { // removes the 'disabled' tag from all time inputs
     1209      for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = false }
     1210      for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = false }
     1211   }
     1212
     1213   function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs
     1214      for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = true; startTimeInput.childNodes[idx].value = 0; }
     1215      for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = true; endTimeInput.childNodes[idx].value = 0; }
     1216   }
     1217
     1218   function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination
     1219      isZooming = true;
     1220      speakerCheckbox.disabled = true;
     1221      let isOut = false;
     1222      if (dest == 0) isOut = true;
     1223      zoomInterval = setInterval(() => {
     1224         if (isOut) {
     1225            if (zoomSlider.value != 0) {
     1226               if (zoomSlider.value > 50) zoomSlider.value -= 30; // ramp up for finer adjustments
     1227               else zoomSlider.stepDown();
     1228               wavesurfer.zoom(zoomSlider.value / 4);
     1229            } else {
     1230               clearInterval(zoomInterval);
     1231               isZooming = false;
     1232               speakerCheckbox.disabled = false;
     1233               zoomSlider.dispatchEvent(new Event("input"));
     1234            }
     1235         } else {
     1236            if (zoomSlider.value / 4 < dest) {
     1237               if (zoomSlider.value > 50) zoomSlider.value += 30; // ramp up for finer adjustments
     1238               else zoomSlider.stepUp();
     1239               wavesurfer.zoom(zoomSlider.value / 4);
     1240            } else {
     1241               clearInterval(zoomInterval);
     1242               isZooming = false;
     1243               speakerCheckbox.disabled = false;
     1244               zoomSlider.dispatchEvent(new Event("input"));
     1245            }
     1246         }
     1247      }, 10); // interval
     1248     
     1249   }
     1250
     1251   function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects
     1252      if (!saveButton.classList.contains("disabled")) {
     1253         currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects);
     1254         editsMade = false;
     1255         removeCurrentRegion();
     1256         reloadRegionsAndChapters();
     1257         console.log("saved changes");
     1258      }
     1259   }
     1260
     1261   function discardRegionChanges() { // resets tempSpeakerObjects to speakerObjects
     1262      if (!discardButton.classList.contains("disabled")) {
     1263         let confirm = window.confirm("Are you sure you want to discard changes?");
     1264         if (confirm) {
     1265            currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects);
     1266            editsMade = false;
     1267            removeCurrentRegion();
     1268            resetUndoStates();
     1269            reloadRegionsAndChapters();
     1270            console.log("discarded changes");
     1271         }
     1272      }
     1273   }
     1274
     1275   function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions
     1276      updateRegionEditPanel();
     1277      wavesurfer.clearRegions();
     1278      $(".region-top").remove();
     1279      $(".region-bottom").remove();
     1280      populateChapters(primarySet);
     1281      if (dualMode) {
     1282         populateChapters(secondarySet);
     1283         currSpeakerSet = primarySet;
     1284      }
     1285      updateCurrSpeakerSet();
     1286      if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
     1287         hoverSpeaker.innerHTML = currentRegion.speaker;
     1288         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     1289         removeRegionBounds();
     1290         let currIndexes = getCurrentRegionsIndexes();
     1291         for (let i = 0; i < currIndexes.length; i++) {
     1292            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
     1293         }
     1294         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
     1295      }
     1296      if (currentRegions.length < 1) {
     1297         removeButton.innerHTML = "Remove Selected Region";
     1298         enableStartEndInputs();
     1299      } else {
     1300         removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")";
     1301         disableStartEndInputs();
     1302         const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
     1303         uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
     1304         // console.log(uniqueSelectedSpeakers); // CLG
     1305         speakerInput.value = uniqueSelectedSpeakers.join(", ");
     1306      }
     1307   }
     1308
     1309   function changeStartEndTime(e) { // start/end time input handler
     1310      let newStart = getTimeInSecondsFromInput(startTimeInput);
     1311      let newEnd = getTimeInSecondsFromInput(endTimeInput);
     1312      let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio
     1313
     1314      if (getCurrentRegionIndex() != -1) { // if there is a selected region
     1315         if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward
     1316         if (newEnd <= 0) newEnd = 1;
     1317         if (newStart < 0) newStart = 0; // ensures region start doesn't go < 0s
     1318         if (newEnd > duration) newEnd = duration; // ensures region start doesn't go > duration
     1319         
     1320         setInputInSeconds(startTimeInput, newStart);
     1321         setInputInSeconds(endTimeInput, newEnd);
     1322
     1323         let currRegIdx = getCurrentRegionIndex();
     1324         currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart;
     1325         currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd;
     1326         currentRegion.start = newStart;
     1327         currentRegion.end = newEnd;
     1328         editsMade = true;
     1329         reloadRegionsAndChapters();
     1330         handleSameSpeakerOverlap(currRegIdx);
     1331         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "change-time", getCurrentRegionIndex());
     1332      } else {
     1333         console.log("no region selected");
     1334         setInputInSeconds(startTimeInput, 0);
     1335         setInputInSeconds(endTimeInput, 0);
     1336      }
     1337   }
     1338
     1339   function getTimeInSecondsFromInput(input) { // returns time in seconds from start or end input
     1340      let hours = input.children[0].valueAsNumber;
     1341      let mins = input.children[1].valueAsNumber;
     1342      let secs = input.children[2].valueAsNumber;
     1343      return (hours * 3600) + (mins * 60) + secs;
     1344   }
     1345
     1346   function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds
     1347      let date = new Date(null);
     1348      date.setMilliseconds(seconds * 1000);
     1349      input.children[0].value = date.getHours() % 12;
     1350      input.children[1].value = date.getMinutes();
     1351      input.children[2].value = date.getSeconds() + "." + date.getMilliseconds();
     1352     
     1353      document.querySelectorAll('input[type=number]').forEach(e => {
     1354         e.value = Math.round(e.valueAsNumber * 10) / 10; // to 1dp
     1355         if (e.classList.contains("seconds") && !e.value.includes(".")) e.value = e.value + ".0";
     1356         else if (e.value.length === 1) e.value = '0' + e.value; // 0 on left for hrs & mins
     1357         if (e.value.length === 3) e.value = '0' + e.value; // 0 on the left (doesn't work on FF)
     1358         // if (e.value.length < 4) e.value = e.value.slice(0, 4); // always 4 digits (3 numbers, 1 fullstop)
     1359         // if (!e.value) e.value = '00'; // avoiding letters on FF
     1360      });     
     1361   }
     1362
     1363   function addUndoState(state, secState, isSec, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
     1364      let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references
     1365      let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references
     1366      undoButton.classList.remove("disabled");
     1367      undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made
     1368      undoStates.push({state: newState, secState: newSecState, isSec: isSec, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});
     1369      if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously
     1370         undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits)
     1371         prevUndoState = type;
     1372      } else undoLevel++;
     1373      prevUndoState = type;
     1374      redoButton.classList.add("disabled");
     1375      // console.log(undoStates.at(-1));
     1376   }
     1377
     1378   function undo() { // undo action: go back one state in the undoStates stack
     1379      if (!undoButton.classList.contains("disabled")) { // ensure there exist states to undo to
     1380         if (undoLevel - 1 < 0) console.log("ran out of undos");
     1381         else {           
     1382            let adjustedUndoLevel = undoLevel-1;
     1383            primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays
     1384            if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists
     1385               secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays
     1386            }
     1387            editsMade = true;
     1388           
     1389            let selectedSpeakerSet;
     1390           
     1391            // handle currentRegion change
     1392            removeCurrentRegion(); 
     1393            if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove
     1394               selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet;
     1395               if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1396               else caretClicked("primary-caret");
     1397               currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state
     1398               // console.log("undo-ing to index " + undoStates[undoLevel].currentRegionIndex);
     1399            } else if (undoStates[undoLevel-1].currentRegionIndex) {
     1400               if (!dualMode) selectedSpeakerSet = primarySet;
     1401               else {
     1402                  selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet;
     1403                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1404                  else caretClicked("primary-caret");
     1405               }
     1406               currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel-1].currentRegionIndex]; // restore previous current state
     1407               // console.log("undo-ing to index " + undoStates[undoLevel-1].currentRegionIndex);
     1408            }
     1409            // handle currentRegions change NEEDS REVISION xxxxx
     1410            if (undoStates[undoLevel-1].currentRegionIndexes && undoStates[undoLevel-1].currentRegionIndexes.length > 1) {
     1411               for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
     1412
     1413               // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
     1414               // if (!speakerCheckbox.checked) speakerCheckbox.click(); // manually fires onChange event
     1415            }
     1416            reloadRegionsAndChapters();
     1417            undoLevel--; // decrement undoLevel
     1418            if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
     1419            else undoButton.classList.remove("disabled");
     1420         }
     1421         if (undoLevel < undoStates.length) redoButton.classList.remove("disabled");
     1422      }
     1423   }
     1424
     1425   function redo() { // redo action: go forward one state in the undoStates stack
     1426      if (!redoButton.classList.contains("disabled")) { // ensure there exist states to redo to
     1427         if (undoLevel + 1 > undoStates.length - 1) console.log("ran out of redos");
     1428         else {
     1429            primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state
     1430            secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state
     1431            editsMade = true;
     1432            let selectedSpeakerSet;
     1433
     1434            // handle currentRegion change
     1435            if (undoLevel+1 <= undoStates.length-1) {
     1436               removeCurrentRegion();
     1437               if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") {
     1438                  selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet;
     1439                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1440                  else caretClicked("primary-caret");
     1441                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex];
     1442               } else {
     1443                  selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet;
     1444                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1445                  else caretClicked("primary-caret");
     1446                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex];
     1447               }
     1448
     1449               // console.log("redo-ing to index " + undoStates[undoLevel+1].currentRegionIndex);
     1450               if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) {
     1451                  for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
     1452                  // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
     1453                  // if (!speakerCheckbox.checked) speakerCheckbox.click(); // ensures onchange event is fired
     1454               }
     1455            }
     1456           
     1457            reloadRegionsAndChapters();
     1458            undoLevel++; // increment undoLevel
     1459            if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled");
     1460            else redoButton.classList.remove("disabled");
     1461         }
     1462         if (undoLevel < undoStates.length) undoButton.classList.remove("disabled");
     1463         // console.log("new undoLevel: " + undoLevel);
     1464      }
     1465   }
     1466
     1467   function resetUndoStates() { // clear undo history
     1468      undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}];
     1469      undoLevel = 0;
     1470      undoButton.classList.add("disabled");
     1471      redoButton.classList.add("disabled");
     1472   }
     1473
     1474   function waveformScrolled() { // waveform scroll handler
     1475      if (currentRegion.speaker && getCurrentRegionIndex() != -1) { // updates region bound markers if selected region exists
     1476         hoverSpeaker.innerHTML = currentRegion.speaker;
     1477         hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     1478         removeRegionBounds();
     1479         let currIndexes = getCurrentRegionsIndexes();
     1480         for (let i = 0; i < currIndexes.length; i++) {
     1481            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
     1482         }
     1483         drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
     1484      }
     1485   }
     1486
     1487   function drawRegionBounds(region, scrollPos) { // draws on canvas to show bounds of hovered/selected region
     1488      const hoverSpeakerCanvas = document.createElement("canvas");
     1489      let colour = "black";
     1490      hoverSpeakerCanvas.id = "hover-speaker-canvas";
     1491      hoverSpeakerCanvas.classList.add("region-bounds");
     1492      hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds
     1493      const ctx = hoverSpeakerCanvas.getContext("2d");
     1494
     1495      ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency
     1496      ctx.lineWidth = 1;
     1497      if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region)) {
     1498         colour = "FireBrick";
     1499         ctx.lineWidth = 3;
     1500      }
     1501      ctx.strokeStyle = colour;
     1502      ctx.beginPath();
     1503      ctx.moveTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 28);
     1504      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 20);
     1505      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 20);
     1506      ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 28);
     1507      ctx.stroke();
     1508      audioContainer.prepend(hoverSpeakerCanvas);
     1509   }
     1510
     1511   function removeRegionBounds() { // remove all region bound markers
     1512      let canvases = document.getElementsByClassName('region-bounds');
     1513      while (canvases[0]) canvases[0].parentNode.removeChild(canvases[0]);
     1514   }
     1515
     1516   function updateCurrSpeakerSet() {
     1517      if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet;
     1518      else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet;
     1519   }
     1520
     1521   function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions)
     1522      let output = [];
     1523      for (let i = 0; i < inputArray.length; i++) { output.push({speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end }) }
     1524      return output;
     1525   }
     1526
     1527   function flashInput(valid) { // flashes background of input to show validity of input
     1528      if (valid) speakerInput.style.backgroundColor = "rgb(50,255,50)";
     1529      else speakerInput.style.backgroundColor = "rgb(255,50,50)";
     1530      setTimeout(() => { speakerInput.style.backgroundColor = "rgb(255,255,255)" }, 750);
     1531   }
     1532
     1533   function flashChapters() {
     1534      chapters.style.backgroundColor = "rgb(66, 84, 88)";
     1535      setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500);
     1536   }
     1537
     1538   function fullscreenChanged() { // fullscreen onChange handler, increases waveform height & adjusts padding/margin
     1539      if (!audioContainer.classList.contains("fullscreen")) {
     1540         audioContainer.classList.add("fullscreen");
     1541         wavesurfer.setHeight(175);
     1542      } else  {
     1543         audioContainer.classList.remove("fullscreen");
     1544         wavesurfer.setHeight(128);
     1545      }
     1546   }
     1547
     1548   function toggleFullscreen() { // toggles fullscreen mode of audio player/editor
     1549      if ((document.fullscreenElement && document.fullscreenElement !== null) ||
     1550        (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) ||
     1551        (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
     1552        (document.msFullscreenElement && document.msFullscreenElement !== null)) {
     1553         document.exitFullscreen();
     1554      } else {
     1555         audioContainer.requestFullscreen();
     1556      }
     1557   }
    6031558}
    6041559
    6051560function formatAudioDuration(duration) {
    606    console.log(duration);
    607    var [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
     1561   // console.log('duration: ' + duration);
     1562   let [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
    6081563   return hrs + ":" + mins + ":" + secs;
    6091564}
  • main/trunk/greenstone3/web/interfaces/default/style/core.css

    r37003 r37031  
    13921392#audioContainer {
    13931393    width: 100%;
    1394     /* background-color: rgb(24, 36, 39); */
    1395     scrollbar-color: white transparent;
     1394        overflow: hidden;
     1395        font-family: 'Courier New', monospace;
     1396}
     1397
     1398#audioContainer::backdrop {
     1399    background-color: rgb(245, 243, 229);
     1400}
     1401
     1402.fullscreen {
     1403    padding-top: 20vh;
     1404    padding-left: 5vw;
     1405    padding-right: 5vw;
    13961406}
    13971407
     
    14011411
    14021412#toolbar {
    1403     background-color: rgb(24, 36, 39);
     1413    background-color: rgb(20, 30, 32);
    14041414    position: relative;
    14051415    display: flex;
     
    14261436
    14271437#audioContainer img:hover {
    1428     filter: invert(0.6);
     1438    filter: invert(0.5);
    14291439}
    14301440
    14311441#audioContainer img:active {
    1432     filter: invert(0.5);
     1442    filter: invert(0.4);
    14331443}
    14341444
     
    14381448    max-height: 30vh;
    14391449    font-size: 14px;
    1440     background-color: rgb(54, 73, 78);
     1450    background-color: rgb(40, 54, 58);
    14411451    color: white;
    14421452    overflow-y: scroll;
    1443     transition: height 0.4s ease;
     1453        /* transition: background-color 0.4s ease-in-out;
     1454    transition: height 0.4s ease; */
     1455        transition: 0.3s ease-in-out;
     1456        cursor: wait;
     1457        user-select: none;
    14441458}
     1459
     1460/* .audio-scroll::-webkit-scrollbar {
     1461    height: 8px;
     1462    width: 8px;
     1463    background: rgb(24, 36, 39);
     1464}
     1465
     1466.audio-scroll::-webkit-scrollbar-thumb {
     1467    background: #66d640;
     1468} */
    14451469
    14461470.chapter {
     
    14501474    border-top-right-radius: 5px;
    14511475    border-top-left-radius: 5px;
    1452     padding: 0.5em;
     1476    padding: 0.5rem;
    14531477    transition: 0.1s ease;
     1478       
    14541479}
    14551480
     
    14591484}
    14601485
     1486.selected-chapter {
     1487    background-color: rgba(255, 50, 50, 0.5);
     1488}
     1489
     1490.selected-chapter:hover {
     1491    background-color: rgba(255, 100, 100, 0.5);
     1492}
     1493
    14611494#playPauseButton {
    1462     padding-left: 1em;
    1463     padding-right: 1em;
    1464 }
    1465 
    1466 #downloadButton {
    1467     transform: scale(0.8);
    1468     padding-right: 0.75em;
    1469 }
    1470 
    1471 .wavesurfer-region {
    1472     cursor: pointer !important;
    1473     /* border: 1px solid rgba(255, 255, 255, 0.3) !important; */
    1474     transition: 0.1s ease;
     1495    padding-left: 1rem;
     1496    padding-right: 1rem;
     1497}
     1498
     1499#downloadButton, #editButton, #fullscreenButton {
     1500    transform: scale(0.75);
     1501    padding-right: 0.75rem;
     1502}
     1503
     1504#zoomInButton, #zoomOutButton {
     1505    transform: scale(0.75);
     1506}
     1507
     1508#muteButton {
     1509    padding-right: 0.25rem;
     1510}
     1511
     1512#volume-slider {
     1513    position: absolute;
     1514    display: none;
     1515    cursor: pointer;
     1516    z-index: 10;
     1517    height: 8rem;
     1518    margin-top: -8rem;
     1519    padding-left: 1rem;
     1520    padding-right: 1rem;
     1521    margin-left: -1rem;
     1522    box-shadow: 0 0 15px;
     1523}
     1524
     1525#volume-container {
     1526    position: relative;
     1527    display: flex;
     1528    flex-direction: row;
     1529}
     1530
     1531#volume-container:hover #volume-slider {
     1532    display: inline;
     1533}
     1534
     1535canvas {
     1536    /* transition: width 0.5s ease; */
     1537    /* z-index: 4 !important; */
     1538    pointer-events: none;
    14751539}
    14761540
     
    14791543}
    14801544
    1481 #slider {
    1482     width: 10em;
    1483     margin-left: 0.5em;
    1484     margin-right: 0.5em;
     1545#zoom-slider {
     1546    width: 10rem;
     1547    margin-left: 0.5rem;
     1548    margin-right: 0.5rem;
    14851549    cursor: pointer;
    14861550}
    14871551
    1488 #zoomIcon {
    1489     width: 1.2em !important;
    1490 }
    1491 
    14921552#chapterButton {
    1493     padding-left: 0.2em;
    1494     padding-right: 0.75em;
     1553    padding-left: 0.2rem;
     1554    padding-right: 0.75rem;
    14951555}
    14961556
    14971557#wave-timeline {
    1498     background-color: rgb(54, 73, 78);
     1558    background-color: rgb(40, 54, 58);
     1559    /* background-color: rgb(24, 36, 39); */
    14991560}
    15001561
    15011562#waveform-loader {
    15021563    color: white;
    1503     padding: 0.2em;
     1564    padding: 0.2rem;
    15041565}
    15051566
     
    15351596    flex-wrap: wrap;
    15361597    justify-content: space-evenly;
    1537     padding: 0.5em;
     1598    padding: 0.5rem;
    15381599}
    15391600
    15401601.word {
    1541     margin-right: 0.5em;
     1602    margin-right: 0.5rem;
    15421603    cursor: pointer;
    15431604}
     
    15551616
    15561617#tapeDetails td {
    1557     padding: 0.2em;
     1618    padding: 0.2rem;
    15581619}
    15591620
    15601621#tapeDetails td:nth-child(1) {
    1561     padding-right: 6em;
     1622    padding-right: 6rem;
    15621623}
    15631624
     
    15781639.metadataTable td {
    15791640    padding: 2px;
     1641<<<<<<< .mine
     1642}
     1643
     1644/* edit functionality */
     1645#edit-panel {
     1646    width: 50%;
     1647    height: 0px;
     1648    position: relative;
     1649    right: 0;
     1650    max-height: 30vh;
     1651    font-size: 15px;
     1652    background-color: rgb(40, 54, 58);
     1653    color: white;
     1654    overflow-y: auto;
     1655    transition: height 0.4s ease;
     1656    box-sizing: border-box; /* ensures padding doesn't modify width */
     1657    font-family: 'Courier New', monospace;
     1658    font-size: 0.85rem;
     1659    border-left: 1px solid rgb(24, 36, 39);
     1660
     1661    display: flex;
     1662    flex-direction: column;
     1663    flex-wrap: nowrap;
     1664    justify-content: space-between;
     1665}
     1666
     1667#edit-panel button {
     1668    padding: 0.5rem;
     1669    /* border: 1px solid white; */
     1670    cursor: pointer;
     1671    background-color: #F8C537;
     1672    transition: 0.1s ease-in-out;
     1673    font-size: 15px;
     1674    font-family: 'Courier New', monospace;
     1675}
     1676
     1677#edit-panel h3 {
     1678    font-size: 1.2rem;
     1679    margin: 0;
     1680}
     1681
     1682#edit-panel button:hover {
     1683    background-color: #af8b26;
     1684}
     1685
     1686.flex-row {
     1687    display: flex;
     1688    flex-direction: row;
     1689    flex-wrap: nowrap;
     1690}
     1691
     1692#edit-panel input[type='text'] {
     1693    width: 50%;
     1694  /* border: 1px solid white; */
     1695  outline: none;
     1696    font-size: 15px;
     1697    margin-right: 0.5rem;
     1698    margin-bottom: 0.25rem;
     1699    transition: 0.25s ease-in;
     1700    background-color: white;
     1701}
     1702
     1703#edit-panel input::placeholder {
     1704    color: black;
     1705    opacity: 0.75;
     1706}
     1707
     1708#edit-panel input[type='checkbox'] {
     1709    transform: scale(1.25);
     1710    margin-right: 0.5rem;
     1711    margin-top: -0.25rem;
     1712}
     1713
     1714#edit-panel input[type='number'],
     1715.time-label {
     1716    border: none;
     1717  text-align: center;
     1718  width: 1.5rem;
     1719  padding: 0;
     1720    margin: 0 !important;
     1721    font-size: 15px;
     1722}
     1723
     1724.time-label {
     1725    background-color: transparent !important;
     1726    font-family: 'Courier New', Courier, monospace !important;
     1727    font-size: 14px !important;
     1728}
     1729
     1730.time-label-container {
     1731    width: 20%;
     1732    background-color: transparent;
     1733  display: inline-flex;
     1734}
     1735
     1736#edit-panel input[type='number']:focus {
     1737  border: none;
     1738  outline: none;
     1739}
     1740
     1741#change-all-label {
     1742    /* width: 100%; */
     1743}
     1744
     1745#save-discard {
     1746    /* margin-bottom: 1rem; */
     1747    justify-content: space-between;
     1748}
     1749
     1750#audio-dropdowns {
     1751    width: 100%;
     1752    display: flex;
     1753    flex-direction: row;
     1754    flex-wrap: nowrap;
     1755    justify-content: space-between;
     1756}
     1757
     1758#hover-speaker {
     1759    height: 2rem;
     1760    overflow: hidden;
     1761    font-family: 'Courier New', monospace;
     1762}
     1763
     1764#hover-speaker-canvas {
     1765    position: absolute;
     1766    overflow: hidden;
     1767}
     1768
     1769#region-details {
     1770   
     1771}
     1772
     1773.time-picker {
     1774    background-color: white;
     1775  display: inline-flex;
     1776  border: 1px solid #ccc;
     1777  color: #555;
     1778    margin-bottom: 0.25rem;
     1779}
     1780
     1781.no-arrows {
     1782  -moz-appearance: textfield;
     1783    -webkit-appearance: textfield;
     1784    appearance: textfield;
     1785}
     1786
     1787/* input::-webkit-outer-spin-button,
     1788input::-webkit-inner-spin-button {
     1789    opacity: 0.5;
     1790    pointer-events: none;
     1791} */
     1792 
     1793.seconds {
     1794  width: 3.25rem !important;
     1795}
     1796
     1797.disabled {
     1798    /* background-color: #888 !important; */
     1799    opacity: 0.5;
     1800    filter: grayscale(100%);
     1801    -webkit-filter: grayscale(100%);
     1802    cursor: not-allowed !important;
     1803}
     1804
     1805.wavesurfer-region {
     1806    cursor: pointer !important;
     1807    transition: background-color 0.1s ease;
     1808        z-index: 1 !important;
     1809        height: 70% !important;
     1810        top: 15% !important;
     1811        /* transition: width 2s ease; */
     1812}
     1813
     1814.wavesurfer-handle {
     1815    /* background-color: rgb(255, 0, 0) !important; */
     1816    /* z-index: 20 !important; */
     1817    width: 3px !important;
     1818}
     1819
     1820.region-top {
     1821  height: 30% !important;
     1822  top: 10% !important;
     1823}
     1824
     1825.region-bottom {
     1826    height: 30% !important;
     1827    top: 60% !important;
     1828}
     1829
     1830#caret-container {
     1831    position: absolute;
     1832    /* background-color: #090; */
     1833    height: 128px; /* match waveform */
     1834    width: 20px; /* match gs_content padding */
     1835    flex-direction: column;
     1836    justify-content: space-around;
     1837    left: -4px; /* padding */
     1838    cursor: pointer;
     1839    display: none;
     1840}
     1841
     1842#caret-container img {
     1843    filter: none;
     1844    opacity: 0.85;
     1845}
     1846
     1847#caret-container img:hover {
     1848    filter: none;
     1849    opacity: 1;
     1850}
     1851
     1852#selected-header {
     1853    justify-content: space-between;
     1854    padding-top: 0.5rem;
     1855    padding-bottom: 0.5rem;
    15801856}
    15811857
     
    15831859    color: black;
    15841860}
     1861
Note: See TracChangeset for help on using the changeset viewer.