generated at
scrapbox-pomodoro-2

scrapbox-pomodoroからの変更点
以下をoptionにした
今日のページに記録する
optionで、タイマーの開始時に通知を流せるようにした
通知の内容を変えられるようにした
バグ修正
scrapbox-pomodoroは正常に動いていないはずです
scrapbox-pomodoro-2#6064566a1280f00000016087のことでしょうか?気づいてなかったyosider
そうそれですtakker

2021-04-02 19:19:31 タブを閉じてもタイマーの状態が消えないようにした
端末内の全てのタブでタイマーの状態が同期される

doneタブを閉じても状態が消えないようにする
smartphoneだと勝手にアプリを終了させられたりしてすぐパーになってしまう
local storageを使う
data structure
key: ${this._title}-timer
value: {"running": true, "type": "work", "until", "2021-04-02T08:34:49+09:00"}
別のタブで状態が変化したら、即座に反映させる
running true の状態で現在時刻が until を過ぎたら、通知を出す
false の場合は何もしない

状態遷移を使うことにした
状態が変更されたときに実行する処理
状態を変更する処理
時間経過を監視する部分でやる
イメージ

2021/5/21 15:41yosider
使われていないメンバ変数・メソッドを削除
時間切れ時にもrecordされるように修正
感謝takker

js
(async () => { const {Pomodoro} = await import('/api/code/programming-notes/scrapbox-pomodoro-2/script.js'); new Pomodoro('ぼくのかんがえたさいきょうのたいまー', { workPeriod: 5 * 1000, breakPeriod: 10 * 1000, record: false, start: (title, icon) => { return {title: 'Start pomodoro', body: `With 🍅 by ${title}`, icon,}; }, end: (title, icon, isWork) => { return {title: (isWork ? 'End pomodoro' : 'End a break'), body: `With 🍅 by ${title}`, icon,}; }, }); })();

script.js
import { addMilliseconds, lightFormat, isBefore } from '../date-fns.min.js/script.js'; export class Pomodoro { constructor(title, {icon, workPeriod, breakPeriod, record, start, end} = {}) { // 内部変数の初期化: this._title = title ?? 'Pomodoro Timer'; this._icon = icon ?? 'https://gyazo.com/978797f03cb0112a0a4cafdf02dcdde8/raw'; this._record = record ?? false; this._workPeriod = workPeriod ?? 25 * 60 * 1000; this._breakPeriod = breakPeriod ?? 5 * 60 * 1000; this._getStartNotificationOption = start ?? undefined; this._getEndNotificationOption = end ?? ((title, icon, isWork) => { return {title: (isWork ? 'ポモドーロが完了しました。' : '休憩時間が終了しました。'), body: `With 🍅 by ${title}`, icon,}; }); // page menuを追加する scrapbox.PageMenu.addMenu({ title: this._title, image: this._icon, onClick: () => Pomodoro.waitForPermission(), }); this._onStateChange({}, this._state); this._onStorage(); } // 定数の設定 get _startWorkButton() { return { title: '\uf04b︎ Start Work', onClick: () => this.startWork() };} get _stopWorkButton() { return { title: '\uf04d Stop Work', onClick: () => this.stopWork() };} get _startBreakButton() { return { title: '\uf0f4 Start Break ', onClick: () => this.startBreak() };} get _stopBreakButton() { return { title: '\uf04d Stop Break', onClick: () => this.stopBreak() };} get _timerDisplay() { return { title: '--:--', onClick: () => { } };} get _menu() {return scrapbox.PageMenu(this._title);}
↑毎回PageMenuのobjectを関数から受け取るようにしないと、なぜか途中でobjectが変わってしまう

script.js
startWork() { this._changeStateTo('work'); } startBreak() { this._changeStateTo('break'); } stopWork() { this._changeStateTo('beforebreak'); } stopBreak() { this._changeStateTo('ready'); } // 通知機能の使用許可が下りるまで待つ static waitForPermission() { return new Promise((resolve, reject) => { if (Notification.permission === 'granted') {resolve();return;} if (Notification.permission === 'denied') {reject(Error('Permission denied.'));return;} // UI操作を通じて許可申請を出す const a = document.createElement('a'); a.onclick = async () => { const state = await Notification.requestPermission(); if (state !== 'granted') reject(Error('Permission denied.')); resolve(); }; document.body.appendChild(a); a.click(); a.remove(); }); } // 内部methods get _key() { return `${this._title}-timer`; } get _state() { return toObject(localStorage.getItem(this._key)); } set _state({state, until}) { const old = this._state; localStorage.setItem(this._key, toString({state, until})); this._onStateChange(old, {state, until}); } _onStorage() { // 他のタブでのstorage更新を監視する window.addEventListener('storage', ({key, newValue, oldValue}) => { if (key !== this._key) return; this._onStateChange(toObject(oldValue), toObject(newValue)); }); } // 全てのタブで行うやつ _onStateChange(oldObject, newObject) { switch (newObject.state) { // 残り作業時間を表示する case 'work': this._setItems(this._timerDisplay, this._stopWorkButton); this._setUpdateTimerLoop(); return; // 休憩に入るまでの待機状態 case 'beforebreak': this._setItems(this._startBreakButton); return; // 残り休憩時間を表示する case 'break': this._setItems(this._timerDisplay, this._stopBreakButton); this._setUpdateTimerLoop(); return; // 初期状態に戻す case 'ready': default: this._setItems(this._startWorkButton); return; } } // page menu itemsの更新 _setItems(...items) { this._menu.removeAllItems(); items.forEach(i => this._menu.addItem(i)); } // 状態を切り替える // 状態を切り替えたタブでのみ行う処理も含まれている _changeStateTo(state) { switch (state) { case 'ready': { this._state = {state}; // 通知を出す const {title, ...rest} = this._getEndNotificationOption(this._title, this._image, false); new Notification(title, rest); } break; case 'beforebreak': { this._state = {state}; // 通知を出す const {title, ...rest} = this._getEndNotificationOption(this._title, this._image, true); new Notification(title, rest); // log を記録する if (!this._record) return; const endTime = new Date(); const page = `/${scrapbox.Project.name}/${encodeURIComponent(lightFormat(endTime, 'yyyy/M/d'))}`; const log = `${lightFormat(this._startTime, 'HH:mm')} -> ${lightFormat(endTime, 'HH:mm')}: [${scrapbox.Page.title}]\n`; window.open(`${page}?body=${encodeURIComponent(log)}`); } break; case 'work': { this._startTime = new Date(); this._state = {state, until: addMilliseconds(new Date(), this._workPeriod)}; // 通知を出す if (!this._getStartNotificationOption) return; const {title, ...rest} = this._getStartNotificationOption(this._title, this._image, false); new Notification(title, rest); } break; case 'break': this._state = {state, until: addMilliseconds(new Date(), this._breakPeriod)}; break; } } _setUpdateTimerLoop() { let timer = null; timer = setInterval(() => { const {state, until} = this._state; if ((state !== 'work' && state !== 'break') || !until) { clearInterval(timer); return; } // 時間切れになったら状態を変える const now = new Date(); if (isBefore(until, now)) { clearInterval(timer); this._changeStateTo(state === 'work' ? 'beforebreak' : 'ready'); return; } this._drawTimer(until - now); }, 1000); } _drawTimer(rest) { // itemの0番目に時計があるとして、その時計を更新する this._menu.menus.get(this._title).items[0].title = lightFormat(rest, 'mm:ss'); this._menu.emitChange(); } }

変換用
script.js
function toString({state, until}) { return JSON.stringify({state, ...(until ? {until: until.getTime()} : {})}); } function toObject(objectString) { let json = objectString ? JSON.parse(objectString) : {}; if (json.until) json.until = new Date(json.until); return json; }