generated at
scrapbox-pomodoro-timer-2
Eugene Schwartzの33分33秒を実践するために使うつもり
止めた。代わりにスマホのタイマーを使う
desktop環境を離れる事が多い
smartphoneはいつも持ち歩いているので、時間切れに気付ける

takker用設定
席に座る: 33m33s
休憩: 11m27s
合計でちょうど45minになるようにしてみた


import.js
import {Pomodoro} from './script.js'; new Pomodoro('33m33s', { workPeriod: (33 * 60 + 33) * 1000, breakPeriod: (11 * 60 + 27) * 1000, record: false, start: (title, icon) => { return {title: '椅子に座れ', body: [ '以下の禁止', '・席を離れてはいけない', '・本来の目的以外のことをしてはいけない', '以下の許可', '・その間飲み物を飲んでもいい', '・よそ見をしても構わないし、壁でも窓でも見つめていてもいい', '・33分33秒まったく、何もしなくていもいい', '・作業をしてもいい', `by ${title}`, ].join('\n'), icon,}; }, end: (title, icon, isWork) => { return {title: (isWork ? '作業を中断しろ' : '休憩終わり'), body: [ ...(isWork ? ['休憩ボタンを押せ', '席を離れろ'] : []), `by ${title}` ].join('\n'), 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);} 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(); } } 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; }

#2021-05-21 17:06:23
#2021-04-17 10:12:15
#2021-04-02 19:21:44
#2021-04-01 11:07:08
#2021-03-31 17:57:48