scrapbox-pomodoro-2
以下をoptionにした
optionで、タイマーの開始時に通知を流せるようにした
通知の内容を変えられるようにした
バグ修正
そうそれですtakker.icon
2021-04-02 19:19:31 タブを閉じてもタイマーの状態が消えないようにした
端末内の全てのタブでタイマーの状態が同期される
/icons/done.iconタブを閉じても状態が消えないようにする
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の場合は何もしない
状態遷移を使うことにした
状態が変更されたときに実行する処理
状態を変更する処理
時間経過を監視する部分でやる
イメージ
https://kakeru.app/1fd44221b04c73b0baf2b4c8cdf053be https://i.kakeru.app/1fd44221b04c73b0baf2b4c8cdf053be.svg
2021/5/21 15:41yosider.icon
使われていないメンバ変数・メソッドを削除
時間切れ時にもrecordされるように修正
/icons/感謝.icontakker.icon
code: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,};
},
});
})();
code: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._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が変わってしまう
code: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).items0.title = lightFormat(rest, 'mm:ss'); this._menu.emitChange();
}
}
変換用
code: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;
}