在 table 中选择下一行时更新电子表格边栏中的值

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

为了便于在 Google 电子表格中注释音频文件,我想在边栏中实现一个音频播放器,它会自动播放 URL 行中提及的音频文件一个table。听完并在这一行中输入某个日期后,我想移到下一行并做同样的事情。因此,每当我 select 一个新行时,音频文件的 URL 应该更新,并且整个过程也应该很快,以便一个接一个地快速收听声音文件。

我已经尝试过 中提到的解决方案,但该解决方案依赖于具有时间间隔的轮询函数,这对我来说不切实际,因为它会定期更新侧边栏。对我来说至关重要的是只更新一次侧边栏的内容。

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.
   *
   * 
   *
   * @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>

可访问可重现的示例here;附加组件 > 'play audio'(需要 Google 帐户)。

我正在努力寻找一种方法来仅在 selected 新行时触发侧边栏更新一次。侧边栏的使用不是强制性的,而是另一种解决方案,例如使用自动更新的 'Play' 按钮,也会有帮助。

播放我的音乐

我在我的每个播放列表选择中添加了一个播放此按钮。也许这会帮助你完成你想要的。

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>

诚然,过渡有点粗糙,但我没有在修改上投入太多精力,所以也许你可以稍微平滑一下。只需 运行 launchMusicDiaog() 即可开始。还有一个用于 webapp 的 doGet()。

我对您提供的示例代码做了一些小改动,这样边栏就不会按照时间间隔定期更新。

基本上,我使用 PropertiesService 来存储 selected 的行。这个想法是,脚本检查当前 selected 行和之前 selected 行(最后一次 getRecord 被调用的那个 selected 行,也就是说,在最后一个间隔)是相同的。如果它们相同,则没有一行 selection 更改,这意味着侧边栏中的音频不需要更新。

因此它仅在 selected 行发生变化时更新,我认为这是您遇到的主要问题。

为此,您的代码必须按以下方式修改(有关更改的详细信息,请查看内联注释):

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;
}

取决于是否有select离子变化,getRecord returns:

  • 一个 record 数组,如果 selected 行不同。
  • 字符串 "unchanged",如果 selected 行相同。这可能不是处理此问题的最优雅方式,但您明白了。

然后,showRecord(record)得到这个返回值。如果此值为字符串 "unchanged",则不会更新边栏:

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();
  }

我还在这一行添加了 autoplay 属性:

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

以便在您 select 新行时自动播放音频,而无需单击 play 按钮。

最后,我将 poll 间隔更改为 500,这样您就不必等待新音频播放了。无论如何,您可以将其编辑为最适合您的内容:

interval = interval || 500;

我没有修改脚本的其余部分,尽管它可能会有所改进,因为它主要是为不同的问题编写的。

希望对您有所帮助。