takker-scheduler
Next version
今使ってる
特徴
Scrapboxに書いたページとつなげられる
こいつが一番大きい
smartphoneからでも予定の組み立てと記録ができる
編集が楽
単なるテキストデータ
簡単に書き換えられる
限界
offline環境では使えない
こればっかりはどうしようもないが、大きな弱点でもある
対策:
PC
Vim pluginを作るのが現実的か?
mobile
でscratchから作るしかない?
非常にやりたくない……てか無理ゲー
1からeditorを作れるほどの能力はナイヨー
2021-01-10 20:48:36
[takker-schedulerの開発 2021-01-09]
のようなリンクがタスク名になっていたら、
別ページに切り出すときに
2021-01-09
の部分を取り除くようにした
リンク入力補完をつかってタスクを作れるようにした
実装したいこと
機能を削る
page menuから start task
/ end task
を直接実行できるようにする
1つのボタンにまとめても良さそう
start→end→end解除→start解除の順で切り替わる
転送元に moved
などと記したタスクを残す
できなかったということを記録したい
転送先のリンクを貼っておこうか?
既知の問題
いろいろ検索を冗長にかましたのが原因だと思う
この辺をtuningすれば早くなると思う
検索回数を減らす
moblie用UI
script.jsimport {
goHead, goLine, moveRight,
} from '/api/code/takker/scrapbox-edit-emulation/script.js';
import {press} from '/api/code/takker/scrapbox-keyboard-emulation-2/script.js';
import {cursor} from '/api/code/takker/scrapbox-cursor-position-3/script.js';
import {line as l} from '/api/code/takker/scrapbox-line-info-2/script.js';
import {insertText} from '/api/code/takker/scrapbox-insert-text/script.js';
import {selection} from '/api/code/takker/scrapbox-selection-2/script.js';
import {scrapboxDOM} from '/api/code/takker/scrapbox-dom-accessor/script.js';
import {parse, nextTask, write} from '/api/code/takker/takker-scheduler%2Ftask/script.js';
機能
タスクの追加
script.jsconst interval = 5; // 5 minutes
export function addTask() {
const line = l(cursor().line);
const task = parse({lineNo: line.index});
// 現在行がタスクなら、それと同じ日付にする
// 違ったら今日にする
const date = task?.date ?? new Date();
// 予定開始時刻と見積もり時刻を計算する
// 予定開始時刻は、その前のタスクの見積もり時間にintervalを足したものだけずらしておく
const plan = {
start: task?.plan?.start ? (() => {
let start = new Date(task.plan.start);
start.setMinutes(start.getMinutes() + interval + (task.plan.duration.minutes ?? 0));
return start;
})() : undefined,
duration: task?.plan?.duration,
};
// 書き込む
write({type: 'task', date, plan, lineNo: line.index, comments: []},
// 空白以外が書き込まれていたら、新しい行に書き込む
{overwrite: /^\s+$/.test(line.text) || line.text === ''});
}
タスクの編集
日付
script.js// 予定開始時刻を選択する
export function selectPlanningTime() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
goHead();
moveRight(12); // `YYYY-MM-DD なので12文字
// 5文字選択する
for (let i = 0; i < 5; i++) {
press('ArrowRight', {shiftKey: true});
}
}
// 見積もり時間を選択する
export function selectEstimatedTime() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
goHead();
moveRight(18);// `YYYY-MM-DD hh:mm なので18文字
// 4文字選択する
for (let i = 0; i < 4; i++) {
press('ArrowRight', {shiftKey: true});
}
}
script.jsexport async function walkDay(count = 1) {
if (!selection.exist) {
_walkDay(count);
} else {
const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range;
for (let i = startNo; i <= endNo; i++) {
goLine({index: i});
await sleep(10);
await _walkDay(count);
}
}
}
async function _walkDay(count) {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {date, ...rest} = task;
// 日付をずらして上書きする
let newDate = new Date(date);
newDate.setDate(newDate.getDate() + count);
await write({date: newDate, ...rest}, {overwrite: true});
}
script.jsexport async function moveToday() {
if (!selection.exist) {
_moveToday();
} else {
const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range;
for (let i = startNo; i <= endNo; i++) {
goLine({index: i});
await sleep(10);
await _moveToday();
}
}
}
async function _moveToday() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {date, ...rest} = task;
const now = new Date();
// 日付に変更がなければ何もしない
if (date.getFullYear() === now.getFullYear()
&& date.getMonth() === now.getMonth()
&& date.getDate() === now.getDate()) return;
// 上書きする
await write({date: now, ...rest}, {overwrite: true});
}
予定開始時刻
見積もり時刻
タスクの名称
属性名はページタイトルに入れない
とりあえず現在行のタスクだけ切り出すようにしてみる
うまく実装できたら、選択範囲の全てのタスクに対して実行できるようにしてみる
関数名が微妙
後で変える
script.jsexport async function shrink({targetProject}) {
const data = await _shrink();
if (!data) return;
const {pageTitle, logPageTitle, bodies} = data;
// 個別のpageに切り出す
const body = encodeURIComponent(bodies.join('\n'));
window.open(`https://scrapbox.io/${targetProject}/${pageTitle}?body=${body}`);
// log pageに書き込む
window.open(`https://scrapbox.io/${targetProject}/${logPageTitle}?body=${
encodeURIComponent(`[${pageTitle}]`)
}`);
}
async function _shrink() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return undefined; // タスクでなければ何もしない
const {comments, record, content, lineNo, ...rest} = task;
if (!record.start || !record.end) return undefined; // 終了していないタスクは対象外
const commonTaskName = content.trim()
.replace(/\[(.+?)\s\d{4}-\d{2}-\d{2}\]/, '$1') // 日付を消す
.replace(/[\[\]\n]/g, '');
const newTaskContent = `${commonTaskName} ${toYYYYMMDD(record.start)}`;
const permalink = `[${scrapbox.Page.title}#${scrapboxDOM.lines.children[lineNo].id.slice(1)}]`;
// 上書きする
await write({content: `[${newTaskContent}]`, comments: [], record, lineNo, ...rest}, {overwrite: true});
return {
pageTitle: newTaskContent,
logPageTitle: `Log | ${commonTaskName}`,
bodies: [...(/[\[\]\n]/.test(content) && !/\[(.+?)\s\d{4}-\d{2}-\d{2}\]/.test(content) ? [content] : []),
...comments,
`from ${permalink}`,
`#${toYYYYMMDD(record.start)} ${toHHMMSS(record.start)}`,
],
};
}
記録操作
開始
script.jsexport function startTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (record.end) return;// すでに終了していたら何もしない
// 上書きする
write({record: {
// 開始時刻をtoggleする
start: !record.start ? new Date() : undefined
}, ... rest}, {overwrite: true});
}
終了
script.jsexport async function endTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (!record.start) return;// まだ開始していなかったら何もしない
const newTask = {record: {start: record.start,
// 終了時刻をtoggleする
end: !record.end ? new Date() : undefined,
}, ...rest};
// 上書きする
await write(newTask, {overwrite: true});
if (!newTask.record.end) return;
// repが指定されていたら、次のタスクを作成する
const nextTaskObj = nextTask(newTask)
if (!nextTaskObj) return;
// 新しい行を挿入する
await write(nextTaskObj, {overwrite: false});
}
script.jsexport async function posterioriEndTask() {
const task = parse({lineNo: l(cursor().line).index});
if (!task) return; // タスクでなければ何もしない
const {record, ...rest} = task;
if (record.start || record.end) return; // 開始していないタスクのみが対象
// 直近のタスクを検索する
const lineNo = l(cursor().line).index;
const targetlineNo = [...scrapbox.Page.lines.keys()]
.slice(0, lineNo + 1)
.reverse()
// 終了しているタスクのみ検索する
.find(lineNo => parse({lineNo})?.record?.end);
// 開始時刻と終了時刻を計算する
const end = new Date();
const start = targetlineNo ? parse({lineNo: targetlineNo}).record.end : end;
const newTask = {record: {start, end,}, ...rest};
// 上書きする
await write(newTask, {overwrite: true});
if (!newTask.record.end) return;
// repが指定されていたら、次のタスクを作成する
const nextTaskObj = nextTask(newTask)
if (!nextTaskObj) return;
// 新しい行に挿入する
await write(nextTaskObj, {overwrite: false});
}
繰り返しタスク
タスク名に属性を入れて指定する
タスクの終了時に、次回のタスクを自動生成する
N回分生成する機能もあったほうがいい?
1週間単位で作るときに必要になりそう
script.jsexport async function transport({targetProject}) {
// 転送しないタスクの日付
const diaryDate = parseDiary();
// 検索する範囲
const {startNo, endNo} = selection.exist ? (() => {
const {start,end} = selection.range;
console.log({start,end});
return {startNo: start.lineNo, endNo: end.lineNo};
})() : {startNo: 1, endNo: scrapbox.Page.lines.length - 1};
console.log({startNo, endNo});
// 移動する行を取得する
const targetLines = scrapbox.Page.lines
.slice(startNo, endNo + 1)
.map((line, i) => {
console.log({lineNo: i + startNo});
return {
lineId: scrapboxDOM.lines.children[i + startNo].id,
text: line.text,
date: parse({lineNo: i + startNo})?.date,
};
})
.filter(({date}) => {
if (!date) return false;
if (!diaryDate) return true; // 日付ページでなければ、全てのタスクを転送する
return !(date.getFullYear() === diaryDate.getFullYear()
&& date.getMonth() === diaryDate.getMonth()
&& date.getDate() === diaryDate.getDate());
});
// 対象の行を削除する
for (const {lineId} of targetLines) {
// 行番号だと削除したときにずれてしまう
// 代わりにidを使う
goLine({id: lineId});
await sleep(10);
// 改行を含めて消す
press('End');
press('Home', {shiftKey: true});
press('Home', {shiftKey: true});
press('ArrowLeft', {shiftKey: true});
press('Delete');
}
// 日付ごとにタスクをまとめる
let bodies = {};
targetLines.forEach(({text,date}) => {
bodies[toYYYYMMDD(date)] = {
date,
texts: [...(bodies[toYYYYMMDD(date)]?.texts ?? []), text]
};
});
// 新しいタブで開く
for (const [key, {date, texts}] of Object.entries(bodies)) {
const body = encodeURIComponent(texts.join('\n'));
window.open(`https://scrapbox.io/${targetProject}/${diaryPage(date)}?body=${body}`);
}
}
// 転送先の日付ページの名前
const diaryPage = (date) => `日刊記録sheet ${toYYYYMMDD(date)}`;
function parseDiary() {
if (!/^日刊記録sheet \d{4}-\d{2}-\d{2}$/.test(scrapbox.Page.title)) return undefined;
const [year, month, date] = scrapbox.Page.title
.match(/^日刊記録sheet (\d{4})-(\d{2})-(\d{2})$/).slice(1);
return new Date(year, month - 1, date);
}
見やすくする
実績時刻の時系列順に並び替えることも同時にやる
やること
設計を見直す
タスク操作の設計を整理し直す
parseしたらclassを返す
classを通して操作する
値の書き換え
propertyを使う?
タスクの移動
別ページへの転送
ページへの切り出し
箇条書き部分を切り出す
タイトルはタスク名から属性を抜いたものにする
細かいところは使いながら調節していく
タイムラグが気になる
1. 書き込まれている文字列をparseする
2. classを作る
3. classを通じて書き込まれている文字列を操作する
この2と3の間で書き込み操作が生じていたらバグになる
どうするか
実装を変えないでやりくりする場合
操作を開始するときにclassを作る
使い捨てのinstance
加えて複数人projectで使わないようにする
書き込み操作が生じることを前提として組み立てる場合
……無理じゃね?
まともにやるなら、その行のDOMの変更を監視する?
MutationObserver
を使えばできなくはないだろうけど
そこまでコストを払うほどの脆弱性ではないと思う
前者にする
新しい機能を追加しやすくするために必要
2021-01-07 23:40:55 classにしなくても良い気がしてきた
テキストを解析してobjectを作る
objectと書き込む位置、上書きする領域からテキストを上書きする
いやだめか
タスクは上書きしてもいいけど、箇条書きの方は上書きでテロメアを更新してほしくない
cons: 書き込み関数に渡された変数に関してだけ書き込みを実行すればいいのでは?
差分を検知して、必要な部分だけ書き換えるとか
それこそ面倒では?
無駄に解析を走らせることになる
2021-01-08 08:39:14 これでいく
多少多く解析を走らせたとしても、大した計算量ではないし問題にならないだろう
問題になったときにtuningすればいい。
ファイルを分割する
タスクの解析と書き込み処理をこっちに分ける
具体的なcommandはこのページに書く
新しい機能を追加する
code
Utilities
script.js// 全選択
function selectLine() {
goHead();
press('End', {shiftKey: true});
}
const zero = n => String(n).padStart(2, '0');
function toYYYYMMDD(d) {
return `${d.getFullYear()}-${zero(d.getMonth() + 1)}-${zero(d.getDate())}`;
}
function toHHMMSS(d) {
return `${zero(d.getHours())}:${zero(d.getMinutes())}:${zero(d.getSeconds())}`;
}
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
没コード
classではなく関数で実装することにした
script.js_disabled// lineNo行にある文字列を解析してタスク操作instanceを作る
// undefinedの場合はcursorがいる行を解析する
// タスクでなければ undefinedを返す
function makeTask({lineNo = undefined} = {}){
}
class Task {
constructor({date, plan, record, name, descriptions, lineNo, attributes}) {
this.date = date;
this.plan = plan;
this.record = record;
this.name = name;
this.descriptions = descriptions; // taskの下にぶら下がっている箇条書き
this.lineNo = lineNo; // タスク名がある行の行番号
this.attributes = attributes;
}
get date() {
};
set date() {
};
set duration() {
}
start({time}) {
}
end({time, start = undefined} = {}) {
}
// descriptionsを新しいページに切り出す
shrink() {
}
// タスクを削除する
// 削除したタスクの情報を返却する
// タスクの転送に必要
clear() {
}
}
実装するかわからないアイデア
タイムラインで視覚化する
SVGで
っぽいやつを生成する