Update value in a spreadsheet sidebar when next row is selected in table

老子叫甜甜 提交于 2020-12-13 07:06:24

问题


To facilitate the annotation of audio files in a Google spreadsheet, I'd like to implement an audio player in the sidebar which automatically plays the audio file mentioned as URL in the row of a table. After listening and entering some date in this row, I'd like to move to the next row and do the same. Thus, the URL to the audio file should be updated whenever I select a new row and the whole process should be fast, too, in order to listen quickly to one sound file after the other.

I've experimented with the solution mentioned in this SO post, but this solution is relying on a poll function with a time interval, which is impractical for me as it periodically is updating the sidebar. Crucial for me would be to update the content of the sidebar only once.

Code.gs

var SIDEBAR_TITLE = 'Opnam lauschteren';

/**
 * Adds a custom menu with items to show the sidebar and dialog.
 *
 * @param {Object} e The event parameter for a simple onOpen trigger.
 */
function onOpen(e) {
  SpreadsheetApp.getUi()
      .createAddonMenu()
      .addItem('Opname lauschteren', 'showSidebar')
      .addToUi();
}

/**
 * Runs when the add-on is installed; calls onOpen() to ensure menu creation and
 * any other initializion work is done immediately.
 *
 * @param {Object} e The event parameter for a simple onInstall trigger.
 */
function onInstall(e) {
  onOpen(e);
}

/**
 * Opens a sidebar. The sidebar structure is described in the Sidebar.html
 * project file.
 */
function showSidebar() {
  var ui = HtmlService.createTemplateFromFile('Sidebar')
      .evaluate()
      .setSandboxMode(HtmlService.SandboxMode.IFRAME)
      .setTitle(SIDEBAR_TITLE);
  SpreadsheetApp.getUi().showSidebar(ui);
}

function getValues() {
  var app = SpreadsheetApp;
  var value = app.getActiveSpreadsheet().getActiveSheet().getActiveCell().getValue();
  Logger.log(value);
  return value;
}

function getRecord() {
  // Retrieve and return the information requested by the sidebar.
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var headers = data[0];
  var rowNum = sheet.getActiveCell().getRow();
  if (rowNum > data.length) return [];
  var record = [];
  for (var col=0;col<headers.length;col++) {
    var cellval = data[rowNum-1][col];
    // Dates must be passed as strings - use a fixed format for now
    if (typeof cellval == "object") {
      cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
    }
    // TODO: Format all cell values using SheetConverter library
    record.push({ heading: headers[col],cellval:cellval });
  }
  Logger.log(record);
  return record;
}

Sidebar.html

<!-- Use a templated HTML printing scriptlet to import common stylesheet. -->
<?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>

<!-- Below is the HTML code that defines the sidebar element structure. -->
<div class="sidebar branding-below">
  <!-- The div-table class is used to make a group of divs behave like a table. -->
  <div class="block div-table" id="sidebar-record-block">
  </div>
  <div class="block" id="sidebar-button-bar">
  </div>
  <div id="sidebar-status"></div>
  
  <!-- Use a templated HTML printing scriptlet to import JavaScript. -->
<?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>
</div>

<!-- Enter sidebar bottom-branding below. -->
<div class="sidebar bottom">
  <span class="gray branding-text">PG</span>
</div>

SidebarJavaScript.html

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
  /**
   * Run initializations on sidebar load.
   */
  $(function() {
    // Assign handler functions to sidebar elements here, if needed.

    // Call the server here to retrieve any information needed to build
    // the dialog, if necessary.

    // Start polling for updates        
    poll();
  });

  /**
   * Poll a server-side function at the given interval, to have
   * results passed to a successHandler callback.
   *
   * https://stackoverflow.com/a/24773178/1677912
   *
   * @param {Number} interval   (optional) Time in ms between polls.
   *                            Default is 2s (2000ms)
   */
  function poll(interval) {
    interval = interval || 3000;
    setTimeout(function() {
      google.script.run
        .withSuccessHandler(showRecord)
        .withFailureHandler(
          function(msg, element) {
            showStatus(msg, $('#button-bar'));
            element.disabled = false;
          })
        .getRecord();
    }, interval);
  };

  /**
   * Callback function to display a "record", or row of the spreadsheet.
   *
   * @param {object[]}  Array of field headings & cell values
   */
  function showRecord(record) {
    if (record.length) {
      for (var i = 2; i <= 2; i++) {
        // build field name on the fly, formatted field-1234
        var str = '' + i;
        var fieldId = 'field-' + ('0000' + str).substring(str.length)

        // If this field # doesn't already exist on the page, create it
        if (!$('#'+fieldId).length) {
          var newField = $($.parseHTML('<div id="'+fieldId+'"></div>'));
          $('#sidebar-record-block').append(newField);
        }

        // Replace content of the field div with new record
        $('#'+fieldId).replaceWith('<div id="'+fieldId+'" class="div-table-row"></div>');
        $('#'+fieldId).append($('<div class="div-table-th">' + record[i].heading + '</div>'))
                      .append('<audio id="player" controls > <source src=' + record[i].cellval + ' type=audio/wav >      Your browser does not support the audio element.    </audio>');
      }
    }
    
    // TODO: hide any existing fields that are beyond the current record length

    //Setup the next poll
    poll();
  }

  /**
   * Displays the given status message in the sidebar.
   *
   * @param {String} msg The status message to display.
   * @param {String} classId The message type (class id) that the message
   *   should be displayed as.
   */
  function showStatus(msg, classId) {
    $('#sidebar-status').removeClass().html(msg);
    if (classId) {
      $('#sidebar-status').addClass(classId);
    }
  }

</script>

A reproducible example is accessible here; Add-ons > 'play audio' (Google account necessary).

I am struggling finding a method to trigger the update of the sidebar only once and only when a new row is selected. The use of a sidebar is not mandatory, rather another solution, e.g. with a automatically updated 'Play' button, would be helpful, too.


回答1:


I made some small changes to the example code you provided so that the sidebar does not update periodically following the time interval.

Basically, I've used PropertiesService to store the row that is selected. The idea is that the script checks whether the currently selected row and the previously selected row (the one selected last time getRecord was called, that is, during last interval) are the same. If they are the same, there hasn't been a row selection change, which means the audio in the sidebar doesn't need updating.

So it only updates if the selected row changes, which is, I think, the main issue you are having.

To achieve this, your code would have to be modified in the following way (look at inline comments for details on the changes):

getRecord()

function getRecord() {
  var scriptProperties = PropertiesService.getScriptProperties();
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var headers = data[0];
  var rowNum = sheet.getActiveCell().getRow(); // Get currently selected row
  var oldRowNum = scriptProperties.getProperty("selectedRow"); // Get previously selected row
  if(rowNum == oldRowNum) { // Check if the was a row selection change
    // Function returns the string "unchanged"
    return "unchanged";
  }
  scriptProperties.setProperty("selectedRow", rowNum); // Update row index
  if (rowNum > data.length) return [];
  var record = [];
  for (var col=0;col<headers.length;col++) {
    var cellval = data[rowNum-1][col];
    if (typeof cellval == "object") {
      cellval = Utilities.formatDate(cellval, Session.getScriptTimeZone() , "M/d/yyyy");
    }
    record.push({ heading: headers[col],cellval:cellval });
  }
  return record;
}

Depending on whether there was a selection change, getRecord returns:

  • a record array, if the selected row is different.
  • the string "unchanged", if the selected row is the same. Probably this is not the most elegant way to handle this, but you get the idea.

Then, showRecord(record) gets this returned value. If this value is the string "unchanged", it won't update the sidebar:

showRecord(record)

  function showRecord(record) {
    // Checks whether returned value is `"unchanged"` (this means the row selected is the same one as before)
    if (record != "unchanged" && record.length) {
      for (var i = 2; i <= 2; i++) {
        // build field name on the fly, formatted field-1234
        var str = '' + i;
        var fieldId = 'field-' + ('0000' + str).substring(str.length)

        // If this field # doesn't already exist on the page, create it
        if (!$('#'+fieldId).length) {
          var newField = $($.parseHTML('<div id="'+fieldId+'"></div>'));
          $('#sidebar-record-block').append(newField);
        }

        // Replace content of the field div with new record
        $('#'+fieldId).replaceWith('<div id="'+fieldId+'" class="div-table-row"></div>');
        $('#'+fieldId).append($('<div class="div-table-th">' + record[i].heading + '</div>'))
                      .append('<audio id="player" controls autoplay> <source src=' + record[i].cellval + ' type=audio/wav >      Your browser does not support the audio element.    </audio>');
      }
    }

    // TODO: hide any existing fields that are beyond the current record length

    //Setup the next poll
    poll();
  }

I also added the autoplay attribute in this line:

.append('<audio id="player" controls> <source src=' + record[i].cellval + ' type=audio/wav >      Your browser does not support the audio element.    </audio>')

So that the audio plays automatically when you select a new row, without having to click the play button.

Finally, I changed the poll interval to 500, so that you don't have to wait so much for the new audio to play. Anyway you can edit this to whatever suits you best:

interval = interval || 500;

I didn't modify the rest of the script, even though it can probably be improved owing to the fact that it was mainly written for a different issue.

I hope this is of any help.




回答2:


Playing My Music

I added a play this button to each of my playlist selections. Perhaps this will help you to accomplish what you wish.

code.gs:

function onOpen() {
  SpreadsheetApp.getUi().createMenu('My Music')
  .addItem('Launch Music', 'launchMusicDialog')
  .addToUi();
}

function convMediaToDataUri(filename){
  var filename=filename || "You Make Loving Fun.mp3";//this was my debug song
  var folder=DriveApp.getFolderById("Music Folder Id");
  var files=folder.getFilesByName(filename);
  var n=0;
  while(files.hasNext()) {
    var file=files.next();
    n++;
  }
  if(n==1) {
    var blob=file.getBlob();
    var b64DataUri='data:' + blob.getContentType() + ';base64,' + Utilities.base64Encode(blob.getBytes());
    Logger.log(b64DataUri)
    var fObj={filename:file.getName(),uri:b64DataUri}
    return fObj;
  }
  throw("Multiple Files with same name.");
  return null;
}


function launchMusicDialog() {
  var userInterface=HtmlService.createHtmlOutputFromFile('music1');
  SpreadsheetApp.getUi().showModelessDialog(userInterface, 'Music');
}


function doGet() {
  return HtmlService.createHtmlOutputFromFile('music1').addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function getPlaylist() {
  var ss=SpreadsheetApp.getActive();
  var sh=ss.getSheetByName('MusicList');
  var rg=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn());
  var vA=rg.getValues();
  var pl=[];
  var idx=0;
  var html='<style>th,td{border:1px solid black;}</style><table><tr><th>Index</th><th>Item</th><th>FileName</th><th>&nbsp;</th></tr>';
  for(var i=0;i<vA.length;i++) {
    if(vA[i][4]) {
      pl.push(vA[i][1]);
      html+=Utilities.formatString('<tr><td>%s</td><td>%s</td><td>%s</td><td><input type="button" value="Play This" onClick="playThis(%s)" /></td></tr>',idx,vA[i][0],vA[i][1],idx++);
    }
  }
  html+='</table>';
  return {playlist:pl,html:html};
}

music1.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <style>
      label{margin:2px 10px;}
    </style>
  </head>
  <script>
    var selectionList=["BarbaraAnn.mp3","Don't Let Me Come Home a Stranger.mp3"];
    var gVolume=0.2;
    var index=0;
    $(function(){
       document.getElementById('msg').innerHTML="Loading Playlist";
       google.script.run
       .withSuccessHandler(function(Obj){
         selectionList=Obj.playlist;
         console.log(Obj.playlist);
         document.getElementById('list').innerHTML=Obj.html;
         google.script.run
         .withSuccessHandler(function(fObj){
           $('#audio1').attr('src',fObj.uri);
           var audio=document.getElementById("audio1");
           audio.volume=gVolume;
           audio.onended=function() {
             document.getElementById('status').innerHTML='Ended...';
             playnext();
           }
           var msg=document.getElementById('msg');
           msg.innerHTML="Click play to begin playlist. Additional selections will begin automatically";        
           audio.onplay=function() {
             document.getElementById('msg').innerHTML='Playing: ' + selectionList[index-1];
             document.getElementById('status').innerHTML='Playing...';
             document.getElementById('skipbtn').disabled=false;
           }
           audio.onvolumechange=function(){
             gVolume=audio.volume;
           }         
         })
         .convMediaToDataUri(selectionList[index++]);
       })
       .getPlaylist();
    });

    function playnext() {
      if(index<selectionList.length) {
        document.getElementById('status').innerHTML='Loading...';
        document.getElementById('msg').innerHTML='Next Selection: ' + selectionList[index];
        google.script.run
        .withSuccessHandler(function(fObj){
          $('#audio1').attr('src',fObj.uri);
          var audio=document.getElementById('audio1');
          audio.volume=gVolume;
          audio.play();
        })
        .convMediaToDataUri(selectionList[index++]);
      }else{
        document.getElementById('status').innerHTML='Playlist Complete';
        document.getElementById('msg').innerHTML='';
        document.getElementById('cntrls').innerHTML='<input type="button" value="Replay Playlist" onClick="replayPlaylist()" />';
      }
    }
   function replayPlaylist() {
     index=0;
     document.getElementById('cntrls').innerHTML='';
     playnext();
   }
   function skip() {
     var audio=document.getElementById('audio1');
     document.getElementById('skipbtn').disabled=true;
     audio.pause();
     playnext();
   }
   function playThis(idx) {
     index=idx;
     var audio=document.getElementById('audio1');
     //audio.pause();
     playnext();
   }
  </script>
  <body>
    <div id="msg"></div>
    <audio controls id="audio1" src=""></audio><br />
    <div id="status"></div>
    <div><input type="button" id="skipbtn" value="Skip" onClick="skip()" disabled /></div>
    <div id="cntrls"></div>
    <div id="list"></div>
  </body>
</html>

Admittedly, the transition is a little rough but I didn't put that much effort into the modification so perhaps you can smooth it out a little. Just run launchMusicDiaog() to get it going. There's also a doGet() in there for the webapp.



来源:https://stackoverflow.com/questions/58621104/update-value-in-a-spreadsheet-sidebar-when-next-row-is-selected-in-table

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!