Автобэкап Emotico в Google Диск и Таблицу

Настраивается один раз, около 5 минут. После этого записи сами попадают в твой Google Диск: полная копия для восстановления и таблица для чтения. Удобнее делать с компьютера.

Как это устроено. Emotico хранит данные только на твоём устройстве. Чтобы отправлять копию в Диск, ты создаёшь в своём Google-аккаунте маленький личный скрипт. Он принимает данные от приложения и кладёт их в твой Диск. Никто, кроме тебя, доступа к ним не получает.

1Создай проект

Открой script.google.com под своим Google-аккаунтом и нажми Создать проект.

Кнопка «Создать проект» в Apps Script

2Назови его

Нажми на «Проект без названия» вверху и введи имя, например emotico-backup.

Окно переименования проекта

3Вставь код

Скопируй код кнопкой ниже. В редакторе выдели всё, что там есть (Ctrl+A), вставь код (Ctrl+V) и сохрани (Ctrl+S).

const FILE_NAME = "emotico-backup.json";
const SHEET_NAME = "Emotico - записи";
// Колонка и её ширина. Порядок блоков разбора повторяет экран записи
const COLS = [["Дата", 90], ["Время", 60], ["Эмоция", 140], ["Сила", 55], ["Дополнительные", 190],
  ["Событие", 200], ["Обстоятельства", 200], ["Мысли", 260], ["Тело", 190],
  ["Реакция", 190], ["Нужно", 190], ["Помогает", 200]];
const BLOCK_KEYS = ["event", "situation", "rumination", "body", "doing", "need", "shift"];

function doPost(e) {
  const data = e && e.postData && e.postData.contents;
  if (!data) return out({ ok: false, error: "empty" });
  // Принимаем только то, что похоже на копию Emotico - случайный мусор в Диск не пишем
  let j;
  try {
    j = JSON.parse(data);
    if (j.v !== 1) throw new Error("not emotico");
  } catch (err) {
    return out({ ok: false, error: "bad payload" });
  }
  // Две отправки подряд не должны писать таблицу одновременно
  const lock = LockService.getScriptLock();
  lock.waitLock(20000);
  let sheet = "ok";
  try {
    const files = DriveApp.getFilesByName(FILE_NAME);
    if (files.hasNext()) files.next().setContent(data);
    else DriveApp.createFile(FILE_NAME, data, "application/json");
    // Таблица - удобство для чтения, а не копия: её сбой не должен ронять бэкап
    try { writeSheet(j.entries || []); } catch (err) { sheet = "error: " + err; }
  } finally {
    lock.releaseLock();
  }
  return out({ ok: true, at: new Date().toISOString(), sheet: sheet });
}

// Таблица - зеркало записей: каждый раз переписывается целиком, поэтому правки
// и удаления в приложении доезжают сами. Настройки приложения сюда не попадают
function writeSheet(entries) {
  const sh = getSheet();
  const bits = (en, key) => {
    const v = en.r && en.r[key];
    if (!v) return "";
    return (v.chips || []).concat(v.text ? [v.text] : []).join(", ");
  };
  const rows = entries.map(en => {
    const em = en.emotions || [];
    const t = new Date(en.time);
    return [t, t,
      em.length ? em[0].word : "только событие",
      em.length ? em[0].level : "",
      em.slice(1).map(x => x.word + " " + x.level + "/10").join(" + ")
    ].concat(BLOCK_KEYS.map(k => bits(en, k)));
  });
  const n = COLS.length;
  sh.clearContents();
  sh.getRange(1, 1, 1, n).setValues([COLS.map(c => c[0])]);
  if (rows.length) {
    sh.getRange(2, 1, rows.length, n).setValues(rows);
    sh.getRange(2, 1, rows.length, 1).setNumberFormat("dd.mm.yyyy");
    sh.getRange(2, 2, rows.length, 1).setNumberFormat("hh:mm");
    sh.getRange(2, 1, rows.length, n).setWrap(true).setVerticalAlignment("top");
    sh.getRange(2, 4, rows.length, 1).setHorizontalAlignment("center");
  }
}

function getSheet() {
  const props = PropertiesService.getScriptProperties();
  const id = props.getProperty("SHEET_ID");
  if (id) {
    try {
      if (!DriveApp.getFileById(id).isTrashed()) return SpreadsheetApp.openById(id).getSheets()[0];
    } catch (err) {}
  }
  // Первая отправка (или таблицу удалили) - создаём и оформляем один раз
  const ss = SpreadsheetApp.create(SHEET_NAME);
  props.setProperty("SHEET_ID", ss.getId());
  const sh = ss.getSheets()[0];
  const n = COLS.length;
  sh.setName("Записи");
  sh.setFrozenRows(1);
  sh.getRange(1, 1, 1, n).setFontWeight("bold").setBackground("#2F2A3A").setFontColor("#FFFFFF")
    .setVerticalAlignment("middle");
  sh.setRowHeight(1, 32);
  COLS.forEach((c, i) => sh.setColumnWidth(i + 1, c[1]));
  sh.getRange(2, 1, sh.getMaxRows() - 1, n).applyRowBanding(SpreadsheetApp.BandingTheme.LIGHT_GREY, false, false);
  // Сила 1-10 подсвечена шкалой: динамику видно без графика
  const power = sh.getRange(2, 4, sh.getMaxRows() - 1, 1);
  sh.setConditionalFormatRules([SpreadsheetApp.newConditionalFormatRule()
    .setGradientMinpointWithValue("#FFFFFF", SpreadsheetApp.InterpolationType.NUMBER, "1")
    .setGradientMaxpointWithValue("#F2A65A", SpreadsheetApp.InterpolationType.NUMBER, "10")
    .setRanges([power]).build()]);
  return sh;
}

// Служебная: пересобрать таблицу из последней копии в Диске. Запускается вручную из редактора
// (кнопка «Выполнить») - например, если таблицу удалили и не хочется ждать отправки с телефона
function rebuildFromBackup() {
  const files = DriveApp.getFilesByName(FILE_NAME);
  if (!files.hasNext()) throw new Error("нет файла " + FILE_NAME);
  const j = JSON.parse(files.next().getBlob().getDataAsString());
  writeSheet(j.entries || []);
  Logger.log("строк: " + (j.entries || []).length);
}

function out(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}
Редактор Apps Script со вставленным кодом

4Начни развёртывание

Справа вверху нажми Начать развертывание → Новое развертывание.

Меню «Начать развертывание»

5Выбери тип «Веб-приложение»

Нажми на шестерёнку рядом с «Выберите тип» и выбери Веб-приложение.

Выбор типа развертывания

6Настрой доступ

«Все» нужно, потому что приложение обращается к скрипту без входа в Google. Защитой служит сам адрес скрипта: его знаешь только ты. Затем нажми Начать развертывание.

Настройки: от моего имени, доступ - Все

7Выдай разрешения

Нажми Предоставить доступ и выбери свой аккаунт.

Кнопка «Предоставить доступ»

Google покажет предупреждение «Приложение не проверено». Это ожидаемо: скрипт твой собственный, Google его не проверял. Нажми Дополнительные настройки → Перейти на страницу emotico-backup → Разрешить.

Скрипт просит доступ к Диску и Таблицам - чтобы создать файл копии и таблицу записей.

8Скопируй URL веб-приложения

Под надписью «Веб-приложение» будет адрес вида https://script.google.com/macros/s/…/exec. Скопируй его кнопкой под ним.

URL веб-приложения в окне развертывания
Этот адрес - секрет. Никому его не отправляй и не вставляй на втором устройстве: файл в Диске один, и другое устройство перезапишет твои данные своими.

9Вставь URL в Emotico

На телефоне открой Emotico → Больше → Резервное копирование. Вставь адрес в поле «Автобэкап в Google Диск» и нажми Сохранить. Статус сменится на «Последняя копия: …».

Поле URL автобэкапа в приложении

✓Что появится в Диске

ДатаВремяЭмоцияСилаДополнительныеСобытиеОбстоятельстваМысли
08.10.202613:52спокойствие5общениедом
07.10.202618:10раздражение7усталость 4/10дедлайнработане успеваю

Пример. В настоящей таблице есть ещё колонки «Тело», «Реакция», «Нужно», «Помогает».

Копия отправляется, пока приложение открыто: при запуске и после изменений, не чаще раза в минуту. Таблица каждый раз переписывается целиком, поэтому свои пометки веди на соседнем листе.

Выключить: сохрани в приложении пустое поле URL.