时雨小径 May the Spirit be with you

用 Workflow + Drafts + Google Apps Script 打造便捷的记录流

最有趣的还是过程, 而手腕上的Apple Watch和裤兜里的iPhone也显得更迷人了呢.

最近开始去gym进行力量训练, 需要记录每一次的项目以及运动量. 前两天的流程是当做完4组训练之后, 打开手机的Note记录, 然后晚上回到家以后再记录到Google Sheets里.

如此做了两天, 发现这流程有待改进. 首先是发现做完单项目4组之后, 实在是不愿意拿出手机来打字, 其次每次练完回到家还得机械性地重复进行填表, 也是令我有点不情愿.

我向来是个宁愿一次性用3个小时写脚本也不愿意每次花15分钟复制粘贴的人. 面对这种的重复性的任务, 自然不会妥协. 所以就想了一些自动化的方法, 最终制定了用Workflow + Drafts + Google Apps Script结合的方法, 实现了从Apple Watch/iPhone到Google Sheets的上传记录流程.


Workflow

大名鼎鼎的Workflow是自动化神器, 之前就在用它来做一些比如自动'发送文章到kindle'之类的任务. 利用Apple Watch上Workflow的widget, 可以很方便的输入一些数字并触发相关的其他任务. 在这里不说太多, 文后会附上一些详细介绍的链接.


Drafts

Drafts是一款与Workflow相得益彰的应用, 最初看到介绍它与Worflow搭配使用是在AppSo上的这篇文章:我的「每日回顾」工作流 | 数字生活家. 非常实用, 可以非常方便地在看新闻的时候将一些值得记录或写点感想的文章保存到Day One里, 让碎片化的信息更保值一些.


Google Apps Script

用JavaScript脚本来控制以及修改Google Docs下的文件, 并且有Time-Driven的触发器来保证脚本自动被执行.


Let's Craft!

Step 1: 首先需要创建一个Workflow使我可以输入我的训练记录, 并保存到Drafts.

  • 选择项目

    workflow-1

  • 输入相应的训练量

    workflow-2 workflow-2

  • 计算一些中间变量, 比如说总量

    workflow-3

  • 保存到Drafts里的一个相应UUID的记录里, UUID会由下面Drafts的步骤得到.

    workflow-4

Step 2: 需要在Workflow里的工作其实比较简单, 接下来完成Drafts里的部分.

  • 首先创建一个新的Drafts文件, 并用Copy Current UUID这个Action来复制UUID, 然后粘贴到Workflow相应的field里.

    draft-1

  • Duplicate一个叫做Save To Google Drive的Action, 重命名为Save Training Record To Google Drive Draft Folder, 做一些修改, Filename即上传的文件名, Parent即要上传至的文件夹.

    draft-2 draft-2 draft-2

  • 建立一个Reset Training的Action, 用来重置Drafts文件.

    draft-3 draft-3

Step 3: 在Google Sheets里创建一个script, 以完成最后的抓取文件以及导入记录的工作.

从菜单打开Tools > Script Editor, 创建需要的函数, 以如下为例, myFunction()会从Google Drive的Drafts文件夹下读取未被处理过的文件, ProcessFile()会读取文件里的内容, 并根据所定义的Mapping和Format更新指定表单里相应单元格内的内容. 在确保代码可以正确运行并更新表单之后, 需要添加一个Trigger, 通过Edit->Current Project's triggers, 每15分钟或1小时运行一次即可.

function myFunction() {
  var folder = DriveApp.getFolderById("{google_drive_folder_id}"); // google_drive_folder_id = id of the Drafts folder
  var allFiles = folder.getFiles()
   while (allFiles.hasNext()) {
     var file = allFiles.next();
     var fileName = file.getName()
     if (fileName.indexOf("###")>-1){
       continue;
     }
     else{
       processFile(file)
     }
 }
}

function getData(str) {
  var lines = str.split('\n');
  var len = lines.length;
  var d = {};
  for (var i = 0 ; i < len ; i ++) {
    var line = lines[i].trim();
    if (!line) continue;
    var temp = line.split(':');
    var name = temp[0];
    d[name] = temp[1].split('/');
  }
  return d;
}

function processFile(fi){
  var ss = SpreadsheetApp.openById("{data_sheet_id}"); // replace data_sheet_id with  of spreadsheet that holds the data to be updated with new report data
  var columns = {
    "Dumbbell": 2,
    "Div. Lat Pull Down": 5,
    "Leg Ext. Front": 8,
    "Leg Ext. Back": 11,
    "Rear Delt": 14,
    "Laterial Raise": 17,
    "Seated Row": 20,
    "Conv. Chest Press": 23,
    "Leg Press": 26,
    "Weight": 30
  };
  if (fi) { // proceed if file exists
    var content = fi.getBlob().getDataAsString();
    var data = getData(content);

    var sheet = ss.getSheetByName("{sheet name}"); // replace with sheet name

    var range = sheet.getDataRange();
    var values = range.getValues();
    var today = new Date();
    var timeZone = Session.getScriptTimeZone();
    var today_date = Utilities.formatDate(today, timeZone, 'MM/dd/yyyy')

    var row_num = -1;
    // find row number to insert the data based on current date.
    for (var r=1; r<values.length; r++) {

      var row = values[r];
      var _date = row[0];

      var dateString = Utilities.formatDate(_date, timeZone, 'MM/dd/yyyy')
      if (dateString === today_date){
        row_num = r;
        break;
      }
    }
    row_num += 1;
    for (var key in columns){
      var c = columns[key]
      if (!data[key]) continue;

      for (var i = 0 ; i < data[key].length ; i ++){
        sheet.getRange(row_num, c + i).setValue(data[key][i]);
      }
    }

    // rename the training.txt file so it is not processed on next scheduled run
    // not removing the file here for backup purpose
    fi.setName("###"+(new Date().toString())+".txt");
  }
};

Step 4: 最后, 创建一个Workflow来一键执行第2步中创建好的Drafts Action以上传数据到Google Sheets.

workflow-5

总结

至此就算是完成了. 由于Worfflow里打开Drafts需要iPhone的handoff功能, 所以还不能完全脱离手机而用Apple Watch完成全套流程.
我个人的操作是, 通过Today Widget里的Workflow快捷按钮来记录每一次的训练量. 这样只要划一划加输入些数字就可以了. 输入完了直接Home + 锁屏.

workflow-5

补充

一些关于Workflow和Drafts的链接:

  1. 效率新神器:自动化工作流程 Workflow for iOS 上手详解
  2. 免费了!被苹果收购的顶级神器 Workflow 要怎么玩?
  3. 一款「文字生产力工具」应有的样子