SOURCE CODE

Apps Scriptへ登録するコード

次の3ファイルを、それぞれ対応するApps Scriptファイルへ貼り付けます。各「コードをコピー」ボタンは、表示を開かなくても全文をコピーできます。

公開用の初期コードです。個人の地点やカレンダーの情報は含まれていません。設定後のコードやデプロイしたURLを、公開の場へ転載しないでください。

1. サーバーコード

Apps Scriptのスクリプトファイルへ貼り付けます。表示名は Code.gs または コード.gs で構いません。冒頭の CONFIG で地域などを設定します。

Code.gs 274行 / 11.2 KB ファイルを保存
コード全文を表示
/**
 * 電子ペーパー風ダッシュボード(時計・天気・週間予定・ニュース)
 *
 * GASのWebアプリとして公開し、iPadなどのSafariで開きっぱなしにして使います。
 *
 * ■ はじめに設定するところ
 *   下の CONFIG のうち「★」の付いた項目だけ、お住まいの地域に合わせて書き換えてください。
 *   それ以外は初期値のままで動きます。
 *
 * ■ 初回の準備
 *   1. CONFIG を書き換えて保存
 *   2. 上部の関数選択で「setup」を選び「実行」→ 権限の確認画面で許可
 *      (「Googleで確認されていません」と出ますが、作者はご自身なので
 *        「詳細」→「(安全ではないページ)に移動」で進めて問題ありません)
 *   3. 「デプロイ」→「新しいデプロイ」→ 種類「ウェブアプリ」
 *        次のユーザーとして実行:自分 / アクセスできるユーザー:自分のみ
 *      ※「全員」にすると、URLを知っている人にあなたの予定が見えてしまいます
 *   4. 表示されたURLをiPadのSafariで開く
 *
 * ■ データの出典
 *   天気:MET Norway / Yr(https://www.met.no/ ・CC BY 4.0・商用利用も可)
 *   予定:あなたのGoogleカレンダー(読み取りのみ)
 *   ニュース:NEWS_RSS に設定したRSS(初期値はNHKニュース)
 */

var CONFIG = {
  // ★ 天気の地域。緯度・経度は Googleマップで地点を長押しすると表示されます
  PLACE: '東京都千代田区',  // 画面に出す地名(表示用なので自由に)
  LAT: 35.69,               // 緯度(小数2桁程度で十分)
  LON: 139.75,              // 経度

  // ★ 天気欄をタップしたときに開くページ。空欄ならタップしても何も開きません
  //   例:tenki.jp でお住まいの市区町村のページを開き、そのURLを貼る
  WEATHER_URL: '',

  // ニュースのRSS。初期値はNHKニュース(主要)。他のRSSのURLにも差し替え可
  //   NHKの分野別:cat1 社会/cat3 科学・医療/cat4 政治/cat5 経済/cat6 国際
  NEWS_RSS: 'https://news.web.nhk/n-data/conf/na/rss/cat0.xml',
  NEWS_COUNT: 8,            // ニュースの表示件数

  DAYS: 7,                  // 予定を表示する日数
  REFRESH_MIN: 10,          // 画面の自動更新間隔(分)。短くしすぎないでください
  CALENDAR_EXCLUDE: [],     // 表示しないカレンダー名(例: ['誕生日', '仕事'])

  TZ: 'Asia/Tokyo'
};

function doGet() {
  var t = HtmlService.createTemplateFromFile('index');
  t.refreshMin = CONFIG.REFRESH_MIN;
  t.place = CONFIG.PLACE;
  return t.evaluate()
    .setTitle('ダッシュボード')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1');
}

/**
 * 初回に一度エディタから実行して、権限の許可と動作確認を行う。
 * 実行ログに各パーツの取得結果が出ます。
 */
function setup() {
  var d = getData();
  Logger.log('天気: ' + (d.weather.ok ? 'OK(' + d.weather.data.temp + '°)' : 'NG ' + d.weather.error));
  Logger.log('予定: ' + (d.events.ok ? 'OK(' + d.events.data.length + '日分)' : 'NG ' + d.events.error));
  Logger.log('ニュース: ' + (d.news.ok ? 'OK(' + d.news.data.length + '件)' : 'NG ' + d.news.error));
}

/** 画面から呼ばれる。各パーツが失敗しても他は表示できるよう個別に包む */
function getData() {
  var weather = safe_(getWeather_);
  if (weather.ok) weather.data.url = CONFIG.WEATHER_URL;
  return {
    weather: weather,
    events: safe_(getEvents_),
    news: safe_(getNews_),
    updated: fmt_(new Date(), 'H:mm')
  };
}

function safe_(fn) {
  try {
    return { ok: true, data: fn() };
  } catch (e) {
    return { ok: false, error: String((e && e.message) || e) };
  }
}

function fmt_(d, pattern) {
  return Utilities.formatDate(d, CONFIG.TZ, pattern);
}

function cached_(key, seconds, fn) {
  var cache = CacheService.getScriptCache();
  var hit = cache.get(key);
  if (hit) return JSON.parse(hit);
  var val = fn();
  cache.put(key, JSON.stringify(val), seconds);
  return val;
}

/* ---------- 天気(MET Norway / Yr、APIキー不要) ----------
 *  MET Norwayの利用条件では、連絡先の分かるUser-Agentの送信が必須です。
 *  再配布して多くの方が使う場合は、下の連絡先をご自身のものに変えてください。 */

var WEATHER_UA = 'PNY-Dashboard/1.0 (+https://www.pny.llc/; support@pny.llc)';

function getWeather_() {
  // 地点を変えたらすぐ反映されるよう、キャッシュのキーに座標を含める
  return cached_('weather_metno_v1_' + CONFIG.LAT + '_' + CONFIG.LON, 1800, function () {
    var url = 'https://api.met.no/weatherapi/locationforecast/2.0/complete'
      + '?lat=' + CONFIG.LAT + '&lon=' + CONFIG.LON;
    var res = UrlFetchApp.fetch(url, {
      headers: { 'User-Agent': WEATHER_UA },
      muteHttpExceptions: true
    });
    var code = res.getResponseCode();
    if (code !== 200) throw new Error('met.no ' + code);
    var ts = JSON.parse(res.getContentText()).properties.timeseries;

    // 現在(先頭の時刻)
    var cur = ts[0].data;
    var nowSym = (cur.next_1_hours && cur.next_1_hours.summary.symbol_code)
      || (cur.next_6_hours && cur.next_6_hours.summary.symbol_code) || '';
    var nowW = sym_(nowSym);

    // 日ごとに集約(最高・最低・代表天気・降水確率)
    var byDay = {}, order = [];
    for (var i = 0; i < ts.length; i++) {
      var e = ts[i], dt = new Date(e.time), key = fmt_(dt, 'yyyy-MM-dd');
      if (!byDay[key]) { byDay[key] = { temps: [], pops: [], sym: '', noon: '' }; order.push(key); }
      var b = byDay[key];
      var inst = e.data.instant && e.data.instant.details;
      if (inst && inst.air_temperature != null) b.temps.push(inst.air_temperature);
      var nx = e.data.next_6_hours || e.data.next_1_hours;
      if (nx) {
        if (nx.summary && nx.summary.symbol_code) {
          if (!b.sym) b.sym = nx.summary.symbol_code;
          var h = +fmt_(dt, 'H');
          if (h >= 12 && h <= 15 && !b.noon) b.noon = nx.summary.symbol_code;
        }
        if (nx.details && nx.details.probability_of_precipitation != null) {
          b.pops.push(nx.details.probability_of_precipitation);
        }
      }
    }

    var days = [];
    for (var k = 0; k < order.length && days.length < 7; k++) {
      var b2 = byDay[order[k]];
      if (!b2.temps.length) continue;
      var p = order[k].split('-');
      var dow = '日月火水木金土'.charAt(new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])).getUTCDay());
      var w = sym_(b2.noon || b2.sym);
      days.push({
        label: (+p[1]) + '/' + (+p[2]),
        dow: dow,
        icon: w[0],
        text: w[1],
        max: Math.round(Math.max.apply(null, b2.temps)),
        min: Math.round(Math.min.apply(null, b2.temps)),
        pop: b2.pops.length ? Math.round(Math.max.apply(null, b2.pops)) : null
      });
    }
    return { temp: Math.round(ts[0].data.instant.details.air_temperature), icon: nowW[0], text: nowW[1], days: days };
  });
}

/** MET Norwayの symbol_code → [一文字表記, 日本語]。_day/_night等は無視し、キーワードで判定 */
function sym_(code) {
  if (!code) return ['—', '—'];
  var c = String(code).replace(/_(day|night|polartwilight)$/, '');
  if (/thunder/.test(c)) return ['雷', '雷雨'];
  if (/snow/.test(c))    return ['雪', /heavy/.test(c) ? '大雪' : '雪'];
  if (/sleet/.test(c))   return ['雪', 'みぞれ'];
  if (/rain/.test(c))    return ['雨', /heavy/.test(c) ? '強い雨' : (/shower/.test(c) ? 'にわか雨' : '雨')];
  if (/fog/.test(c))     return ['霧', '霧'];
  if (c === 'cloudy')        return ['曇', '曇り'];
  if (c === 'partlycloudy')  return ['晴', '晴れ時々曇り'];
  if (c === 'fair')          return ['晴', '晴れ'];
  if (c === 'clearsky')      return ['晴', '快晴'];
  return ['—', '—'];
}

/* ---------- 週間予定(Googleカレンダー・読み取りのみ) ---------- */

function getEvents_() {
  var DAY = 86400000;
  var now = new Date();
  var sinceMidnight = ((+fmt_(now, 'H') * 60 + +fmt_(now, 'm')) * 60 + +fmt_(now, 's')) * 1000 + now.getMilliseconds();
  var start = new Date(now.getTime() - sinceMidnight); // 今日0時(日本時間)
  var end = new Date(start.getTime() + CONFIG.DAYS * DAY);

  var days = [];
  var index = {};
  for (var i = 0; i < CONFIG.DAYS; i++) {
    var d = new Date(start.getTime() + i * DAY);
    var key = fmt_(d, 'yyyy-MM-dd');
    index[key] = days.length;
    days.push({
      key: key,
      url: 'https://calendar.google.com/calendar/r/day/' + fmt_(d, 'yyyy/M/d'),
      label: fmt_(d, 'M/d'),
      dow: '日月火水木金土'.charAt(Number(fmt_(d, 'u')) % 7),
      holiday: '',
      items: []
    });
  }

  CalendarApp.getAllCalendars().forEach(function (cal) {
    var name = cal.getName();
    if (CONFIG.CALENDAR_EXCLUDE.indexOf(name) >= 0) return;
    var isHoliday = /祝日|holiday/i.test(name);
    var color = cal.getColor();
    var calId = cal.getId();

    cal.getEvents(start, end).forEach(function (ev) {
      var url = eventUrl_(ev, calId);
      if (ev.isAllDayEvent()) {
        var s = ev.getAllDayStartDate().getTime();
        var e = ev.getAllDayEndDate().getTime();
        for (var t = Math.max(s, start.getTime()); t < e; t += DAY) {
          var k = fmt_(new Date(t), 'yyyy-MM-dd');
          if (!(k in index)) continue;
          if (isHoliday) {
            days[index[k]].holiday = ev.getTitle();
          } else {
            days[index[k]].items.push({ time: '終日', sort: '', title: ev.getTitle(), color: color, url: url });
          }
        }
      } else {
        var st = ev.getStartTime();
        var k2 = fmt_(st.getTime() < start.getTime() ? start : st, 'yyyy-MM-dd');
        if (!(k2 in index)) return;
        days[index[k2]].items.push({
          time: fmt_(st, 'H:mm'),
          sort: fmt_(st, 'HH:mm'),
          title: ev.getTitle(),
          color: color,
          url: url
        });
      }
    });
  });

  days.forEach(function (day) {
    day.items.sort(function (a, b) { return a.sort < b.sort ? -1 : a.sort > b.sort ? 1 : 0; });
  });
  return days;
}

/** 予定の詳細画面へのURL(Googleカレンダーの eid 形式)
 *  ※繰り返し予定では、その回ではなく繰り返しの本体が開くことがあります */
function eventUrl_(ev, calId) {
  var eid = Utilities.base64Encode(ev.getId().split('@')[0] + ' ' + calId).replace(/=+$/, '');
  return 'https://www.google.com/calendar/event?eid=' + encodeURIComponent(eid);
}

/* ---------- ニュース(RSS) ---------- */

function getNews_() {
  return cached_('news_v3_' + CONFIG.NEWS_RSS, 900, function () {
    var xml = UrlFetchApp.fetch(CONFIG.NEWS_RSS).getContentText();
    var channel = XmlService.parse(xml).getRootElement().getChild('channel');
    return channel.getChildren('item').slice(0, CONFIG.NEWS_COUNT).map(function (it) {
      var pd = it.getChildText('pubDate');
      var t = pd ? new Date(pd) : null;
      return {
        title: it.getChildText('title'),
        url: it.getChildText('link') || '',
        time: t && !isNaN(t.getTime()) ? fmt_(t, 'H:mm') : ''
      };
    });
  });
}

SHA-256: cc520dab662099b26daa3156b9f66d097a14e1416813e1c3aa102c3611760c16

2. 画面

Apps ScriptでHTMLファイルを追加し、名前を必ず index にしてから貼り付けます。

index.html 272行 / 11.6 KB ファイルを保存
コード全文を表示
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="utf-8">
<style>
  /* 電子ペーパー風:紙色の地に墨一色。色はこの2つだけ */
  html, body { margin: 0; padding: 0; }
  body {
    background: #e6e4de; color: #111;
    font-family: -apple-system, "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif;
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
  }
  body.night { background: #111; color: #d8d6d0; }

  .wrap {
    display: -webkit-box; display: -webkit-flex; display: flex;
    box-sizing: border-box; height: 100vh; padding: 28px 32px;
  }
  .col-left  { -webkit-flex: 0 0 42%; flex: 0 0 42%; margin-right: 40px;
               display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; }
  .col-right { -webkit-flex: 1; flex: 1; min-width: 0;
               display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; }

  .sub  { color: #6b6963; }
  body.night .sub { color: #7d7b76; }

  /* 見出し:太字+下に一本線 */
  h2 {
    font-size: 15px; font-weight: 700; letter-spacing: .12em;
    margin: 0 0 12px; padding-bottom: 8px; border-bottom: 2px solid #111;
  }
  body.night h2 { border-color: #d8d6d0; }
  section { margin-bottom: 26px; }

  /* 日付と時計 */
  .head { margin-bottom: 26px; }
  #date  { font-size: 18px; font-weight: 600; letter-spacing: .04em; }
  #clock { font-size: 76px; font-weight: 300; line-height: 1; letter-spacing: -2px; margin-top: 6px; }

  /* 天気 */
  .now { display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center;
         margin-bottom: 14px; }
  .now-icon { width: 56px; height: 56px; line-height: 52px; text-align: center; font-size: 28px;
              border: 2px solid #111; margin-right: 16px; box-sizing: border-box; }
  body.night .now-icon { border-color: #d8d6d0; }
  .now-temp { font-size: 44px; font-weight: 300; margin-right: 14px; }
  .now-text { font-size: 16px; }

  .week { display: -webkit-flex; display: flex; border-top: 1px solid #111; border-left: 1px solid #111; }
  body.night .week, body.night .wd { border-color: #555; }
  .wd { -webkit-flex: 1; flex: 1; text-align: center; font-size: 12px; padding: 6px 0;
        border-right: 1px solid #111; border-bottom: 1px solid #111; }
  .wd .ic { font-size: 18px; font-weight: 600; margin: 3px 0; }
  .wd .hi { font-weight: 700; }
  .wd.today { background: #111; color: #e6e4de; }
  .wd.today .sub { color: #b5b3ad; }
  body.night .wd.today { background: #d8d6d0; color: #111; }
  body.night .wd.today .sub { color: #55534e; }

  /* ニュース */
  .news { -webkit-flex: 1; flex: 1; overflow: hidden; margin-bottom: 0; }
  .news ul { list-style: none; margin: 0; padding: 0; }
  .news li { font-size: 15px; line-height: 1.5; padding: 7px 0; border-bottom: 1px solid #c4c2bc;
             display: -webkit-flex; display: flex; }
  body.night .news li { border-color: #333; }
  .news .nt { width: 46px; -webkit-flex-shrink: 0; flex-shrink: 0; font-size: 12px; padding-top: 2px;
              font-variant-numeric: tabular-nums; }

  /* 予定 */
  /* 見出しは固定し、予定の中身だけをスクロールさせる */
  .sched { -webkit-flex: 1; flex: 1; min-height: 0; margin-bottom: 0;
           display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; }
  .sched-body { -webkit-flex: 1; flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; }
  /* 下に続きがあるときだけ出す印。いちばん下までスクロールすると消える */
  #more { display: none; -webkit-flex-shrink: 0; flex-shrink: 0; font-size: 12px; letter-spacing: .08em;
          text-align: center; padding-top: 6px; border-top: 1px solid #111; }
  body.night #more { border-color: #d8d6d0; }
  .day { display: -webkit-flex; display: flex; padding: 10px 0; border-bottom: 1px solid #c4c2bc; }
  body.night .day { border-color: #333; }
  .day:last-child { border-bottom: 0; }
  .day-head { width: 64px; -webkit-flex-shrink: 0; flex-shrink: 0; }
  .day-date { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .day-dow  { font-size: 12px; }
  .day-body { -webkit-flex: 1; flex: 1; min-width: 0; padding-top: 1px; }
  .hol { font-size: 12px; margin-bottom: 2px; }

  .today .day-head { margin: -10px 14px -10px 0; padding: 10px 0 10px 10px; width: 54px;
                     background: #111; color: #e6e4de; }
  .today .day-head .sub { color: #b5b3ad; }
  body.night .today .day-head { background: #d8d6d0; color: #111; }
  body.night .today .day-head .sub { color: #55534e; }

  .item { display: -webkit-flex; display: flex; font-size: 16px; line-height: 1.55; }
  .item-time { width: 52px; -webkit-flex-shrink: 0; flex-shrink: 0; font-size: 13px; padding-top: 2px;
               font-variant-numeric: tabular-nums; }
  .allday { font-size: 11px; border: 1px solid currentColor; padding: 0 4px; margin-right: 8px;
            position: relative; top: -1px; }
  .empty { font-size: 13px; padding-top: 2px; }

  /* タップできる行:見た目は変えず、押した瞬間だけ反転 */
  a.tap { color: inherit; text-decoration: none; display: -webkit-flex; display: flex;
          -webkit-tap-highlight-color: transparent; }
  a.tap:active { background: #111; color: #e6e4de; }
  body.night a.tap:active { background: #d8d6d0; color: #111; }
  .news li { padding: 0; }
  .news li a.tap { padding: 7px 0; width: 100%; }
  a.day-head, a.weather-link { display: block; }

  .credit { font-size: 10px; margin-top: 6px; text-align: right; }
  .credit a { color: inherit; }

  #status { position: fixed; right: 32px; bottom: 10px; font-size: 11px; }

  @media (orientation: portrait) {
    .wrap { display: block; height: auto; }
    .col-left { margin: 0 0 8px; }
    .sched { display: block; }
    .sched-body { overflow: visible; }
  }
</style>
</head>
<body>
<div class="wrap">
  <div class="col-left">
    <div class="head">
      <div id="date"></div>
      <div id="clock">--:--</div>
    </div>

    <section>
      <h2>天気 <span class="sub" style="font-weight:400;letter-spacing:0"><?= place ?></span></h2>
      <div id="weather" class="sub">読み込み中…</div>
      <!-- MET Norwayの利用条件(CC BY 4.0)により出典の表示が必要。消さないでください -->
      <div class="credit sub">Weather data from <a href="https://www.met.no/" target="_blank" rel="noopener">MET Norway</a> (CC BY 4.0)</div>
    </section>

    <section class="news">
      <h2>ニュース</h2>
      <div id="news" class="sub">読み込み中…</div>
    </section>
  </div>

  <div class="col-right">
    <section class="sched">
      <h2>今週の予定</h2>
      <div id="sched-body" class="sched-body">
        <div id="events" class="sub">読み込み中…</div>
      </div>
      <div id="more">▼ 続きあり</div>
    </section>
  </div>
</div>
<div id="status" class="sub"></div>

<script>
  // 古いiPadのSafariでも動くよう、ES5のみで書いている
  var REFRESH_MIN = Number('<?= refreshMin ?>') || 10;
  var DOW = '日月火水木金土';

  function $(id) { return document.getElementById(id); }
  function esc(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }
  function fail(r) { return '<span class="sub">取得できませんでした(' + esc(r.error) + ')</span>'; }
  // http(s) のURLだけリンクにし、新しいタブで開く(ダッシュボード自体は残す)
  function link(url, cls, inner) {
    if (!/^https?:\/\//.test(url || '')) return '<div class="' + cls.replace('tap', '') + '">' + inner + '</div>';
    return '<a class="' + cls + '" href="' + esc(url) + '" target="_blank" rel="noopener">' + inner + '</a>';
  }
  function done(id, html) { $(id).className = ''; $(id).innerHTML = html; }

  function tick() {
    var d = new Date();
    var m = d.getMinutes();
    $('clock').innerHTML = d.getHours() + ':' + (m < 10 ? '0' : '') + m;
    $('date').innerHTML = (d.getMonth() + 1) + '月' + d.getDate() + '日(' + DOW.charAt(d.getDay()) + ')';
    var h = d.getHours();
    document.body.className = (h >= 22 || h < 6) ? 'night' : '';
  }

  function renderWeather(r) {
    if (!r.ok) { $('weather').innerHTML = fail(r); return; }
    var w = r.data, h = '';
    h += '<div class="now"><span class="now-icon">' + esc(w.icon) + '</span>'
       + '<span class="now-temp">' + w.temp + '°</span><span class="now-text">' + esc(w.text) + '</span></div>';
    h += '<div class="week">';
    for (var i = 0; i < w.days.length; i++) {
      var d = w.days[i];
      h += '<div class="wd' + (i === 0 ? ' today' : '') + '">'
         + '<div>' + (i === 0 ? '今日' : esc(d.dow)) + '</div>'
         + '<div class="ic">' + esc(d.icon) + '</div>'
         + '<div><span class="hi">' + d.max + '</span><span class="sub">/' + d.min + '</span></div>'
         + '<div class="sub">' + (d.pop == null ? '' : d.pop + '%') + '</div></div>';
    }
    h += '</div>';
    done('weather', link(w.url, 'weather-link tap', h));
  }

  function renderEvents(r) {
    if (!r.ok) { $('events').innerHTML = fail(r); return; }
    var h = '';
    for (var i = 0; i < r.data.length; i++) {
      var day = r.data[i];
      h += '<div class="day' + (i === 0 ? ' today' : '') + '">';
      h += link(day.url, 'day-head tap', '<div class="day-date">' + esc(day.label) + '</div>'
         + '<div class="day-dow sub">' + (i === 0 ? '今日・' : '') + esc(day.dow) + '</div>');
      h += '<div class="day-body">';
      if (day.holiday) h += '<div class="hol sub">' + esc(day.holiday) + '</div>';
      if (!day.items.length) {
        h += '<div class="empty sub">予定なし</div>';
      } else {
        for (var j = 0; j < day.items.length; j++) {
          var it = day.items[j];
          if (it.time === '終日') {
            h += link(it.url, 'item tap', '<span><span class="allday">終日</span>' + esc(it.title) + '</span>');
          } else {
            h += link(it.url, 'item tap', '<span class="item-time sub">' + esc(it.time) + '</span><span>' + esc(it.title) + '</span>');
          }
        }
      }
      h += '</div></div>';
    }
    done('events', h);
    updateMore();
  }

  // 予定の欄の下に続きがあるときだけ「▼ 続きあり」を出す(縦向きでは欄がスクロールしないので出ない)
  function updateMore() {
    var b = $('sched-body');
    var rest = b.scrollHeight - b.scrollTop - b.clientHeight;
    $('more').style.display = rest > 4 ? 'block' : 'none';
  }

  function renderNews(r) {
    if (!r.ok) { $('news').innerHTML = fail(r); return; }
    var h = '<ul>';
    for (var i = 0; i < r.data.length; i++) {
      h += '<li>' + link(r.data[i].url, 'tap', '<span class="nt sub">' + esc(r.data[i].time) + '</span><span>' + esc(r.data[i].title) + '</span>') + '</li>';
    }
    h += '</ul>';
    done('news', h);
  }

  function load() {
    google.script.run
      .withSuccessHandler(function (data) {
        renderWeather(data.weather);
        renderEvents(data.events);
        renderNews(data.news);
        $('status').innerHTML = '更新 ' + data.updated;
      })
      .withFailureHandler(function () {
        $('status').innerHTML = '更新に失敗しました(次回再試行)';
      })
      .getData();
  }

  $('sched-body').addEventListener('scroll', updateMore, false);
  window.addEventListener('resize', updateMore, false);
  window.addEventListener('orientationchange', function () { setTimeout(updateMore, 300); }, false);

  tick();
  load();
  setInterval(tick, 20000);
  setInterval(load, REFRESH_MIN * 60000);
</script>
</body>
</html>

SHA-256: 6e489a94d9314e5566f899b4bdcbed455adbc95769dfbae20648c75c964facf4

3. マニフェスト

[プロジェクトの設定]で「『appsscript.json』マニフェスト ファイルをエディタで表示する」をオンにし、表示された appsscript.json の中身と置き換えます。

appsscript.json 13行 / 0.3 KB ファイルを保存
コード全文を表示
{
  "timeZone": "Asia/Tokyo",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/calendar.readonly",
    "https://www.googleapis.com/auth/script.external_request"
  ],
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "MYSELF"
  }
}

SHA-256: b69fd8c52fbfdd915694ba096152d769bdd8f8b09b8bb1c89fa0fbd52d187e12