generated at
takker-scheduler-2
this is a deprecated script

hr
Scrapboxで動くtakkerタスク管理ツールその2
一から、は言い過ぎだけど、いろいろ作り直している

変更点
1行1taskに戻した
以前は、tritask task blockを解析していた
これを無視するようにした
こいつの解析が結構重かった
tritask task blockは別のUserScriptで操作する

実装したいこと
機能を削る
doneTritaskの記録を別ページに切り出す機能を別のUserScriptとして分離する
これは順番に実装する
2. tritask task lineだったときに特別な処理をするようにする
custom-new-pageを使ってScrapBindings-settingsに実装を書いた
donepage menuから start task / end task を直接実行できるようにする
従来は2回タップしないと実行できなかった
4secくらい時間食っていた
1つのボタンにまとめても良さそう
start→end→end解除→start解除の順で切り替わる
転送元に moved などと記したタスクを残す
できなかったということを記録したい
転送先のリンクを貼っておこうか?

moblie用UI

script.js
import { goHead, goLine, moveRight, upBlocks, moveLinesBefore, } from '../scrapbox-edit-emulation/script.js'; import {press} from '../scrapbox-keyboard-emulation-2/script.js'; import {cursor} from '../scrapbox-cursor-position-3/script.js'; import {line as l} from '../scrapbox-line-accessor/script.js'; import {insertText} from '../scrapbox-insert-text/script.js'; import {selection} from '../scrapbox-selection-2/script.js'; import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; import {parse, nextTask, write, create} from '../takker-scheduler-2%2Ftask/script.js'; import {toYYYYMMDD, toHHMM} from '../scrapbox-timestamp/timestamp.js'; import { format, parse as parseDate, isValid, isAfter, set, addDays, subDays, addMinutes, isSameDay, compareAsc, areIntervalsOverlapping, } from '../date-fns.min.js/script.js';

機能
タスクの追加
script.js
const 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 ? (() => addMinutes(task.plan.start, interval + (task.plan.duration.minutes ?? 0)) )() : undefined, duration: task?.plan?.duration, }; // 書き込む write({type: 'task', date, plan, lineNo: line.index}, {overwrite: false}); }
タスクの編集
日付
script.js
export 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; // 日付をずらして上書きする await write({date: addDays(date, count), ...rest}, {overwrite: true}); }
script.js
export 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 (isSameDay(date, now)) return; // 上書きする await write({date: now, ...rest}, {overwrite: true}); }
現在時刻を予定開始時刻として書き込む
もし何も書き込まれていなかったら、空欄にする
script.js
export function togglePlanningTime() { const task = parse({lineNo: l(cursor().line).index}); if (!task) return; // タスクでなければ何もしない const {plan, ...rest} = task; // 上書きする write({plan: { duration: plan.duration, // 予定開始時刻をtoggleする start: !plan.start ? new Date() : undefined, }, ... rest}, {overwrite: true}); }
見積もり時間を書き込む
script.wip.js
// 見積もり時間を選択する 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.js
export 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.js
export 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.js
export async function toggleTask() { const task = parse({lineNo: l(cursor().line).index}); if (!task) return; // タスクでなければ何もしない const {record, ...rest} = task; // 開始していないときは開始する if (!record.start) { write({record: {start: new Date()}, ... rest}, {overwrite: true}); return; } // 終了していないときは終了する if (!record.end) { const newTask = {record: {start: record.start, end: new Date(),}, ...rest}; await write(newTask, {overwrite: true}); // repが指定されていたら、次のタスクを作成する const nextTaskObj = nextTask(newTask); if (!nextTaskObj) return; // 新しい行を挿入する await write(nextTaskObj, {overwrite: false}); return; } // すでに終了しているタスクはリセットする write({record: {}, ... rest}, {overwrite: true}); }
script.js
export 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}); }
並び替え
実績開始時刻順に並び替える
実績開始時刻がなかったら予定開始時刻を使う
それもなかったら00:00として解釈する
Alt+↑/↓ を使って入れ替える
箇条書きもまとめて移動できるようにする
気が向いたらやる
script.js
export async function sort() { if (scrapbox.Page.lines.length < 3) return; const startNo = 2; const endNo = scrapbox.Page.lines.length - 1;
タスク行だけ抽出し、順番を入れ替える
入れ替えるから、行idを持っておく必要がある
script.js
const sortedTasks = scrapbox.Page.lines .slice(startNo, 1 + endNo) .flatMap((line, i) => { const task = parse({lineNo: i + startNo}); return task ? [{id: `L${line.id}`, task}] : []; }) .sort((a,b)=> compareAsc( a.task.record?.start ?? a.task.plan?.start ?? a.task.date, b.task.record?.start ?? b.task.plan?.start ?? b.task.date )); // 一番上から順に入れ替え作業をする for (let i = 0; i < sortedTasks.length; i++) { // taskの順番を計算する const presentPosition = scrapbox.Page.lines .slice(startNo, 1 + endNo) .filter(line => !/^\s+/.test(line.text)) // top level indent lineのみ数える .findIndex(line => `L${line.id}` === sortedTasks[i].id); //console.log({presentPosition}); if (presentPosition === i) continue; goLine(sortedTasks[i].id); await sleep(10); upBlocks(presentPosition - i); } // 見出しもきれいにする insertSections(); }
script.js
async function insertSections() { const sections = [ '[** 00:00 - 03:00] 未明', '[** 03:00 - 06:00] 明け方', '[** 06:00 - 09:00] 朝', '[** 09:00 - 12:00] 昼前', '[** 12:00 - 15:00] 昼過ぎ', '[** 15:00 - 18:00] 夕方', '[** 18:00 - 21:00] 夜のはじめ頃', '[** 21:00 - 00:00] 夜遅く', ]; const pageDate = getPageDate(); if (!isValid(pageDate)) return; // 前の日付ページへのリンクを挿入する const indexLine = `yesterday: [${getDiaryPageTitle(subDays(pageDate, 1))}]`; if (scrapbox.Page.lines.findIndex(line => line.text === indexLine) !== -1) { const index= scrapbox.Page.lines.findIndex(line => line.text === indexLine); goLine(index); await sleep(10); moveLinesBefore({from: index, to: 0}); } else { goLine(0); await sleep(10); press('Enter'); insertText({text: indexLine}); await sleep(1); } // 見出しを挿入する // 挿入位置を計算する const tasks = scrapbox.Page.lines .flatMap((_, lineNo) => { const task = parse({lineNo}); return task ? [task] : []; }); const insertIds = sections.map((_, i) => { // 最初と最後のsectionの挿入位置はわかっている if (i === 0) return {position: 'previous', id: l(tasks[0].lineNo).id}; // sectionの開始・終了時刻 let sectionDates = { start: new Date(pageDate), end: new Date(pageDate), }; sectionDates.start = set(sectionDates.start, {hours: i * 3, minutes: 0}); sectionDates.end = set(sectionDates.end, {hours: (i + 1) * 3, minutes: 0}); // 挿入位置を探す for (const task of tasks) { const {record, plan, date, lineNo} = task; const start = record.start ?? plan.start ?? date; const end = record.end ?? plan.start ?? date; // sectionを跨いでいるタスクの場合 if (areIntervalsOverlapping(sectionDates, {start, end})) { const forward = sectionDates.start.getTime() - start.getTime(); const backward = end.getTime() - sectionDates.start.getTime(); if (forward > backward && isAfter(sectionDates.end, end)) { // sectionのすぐ後ろに入るタスクと判断する //console.log(['border, next',_, l(lineNo).DOM]); return {id: l(lineNo).id}; } else { // sectionのすぐ前に入るタスクと判断する //console.log(['border, previous',_ ,l(lineNo).DOM]); return {position: 'previous', id: l(lineNo).id}; } continue; } // あとはどちらか一方にきれいに収まるタスクである場合しか無い // 前のタスクとの間にあるかどうかを調べる if (isAfter(sectionDates.start, start)) continue; const i = tasks.indexOf(task); const prev = tasks[i - 1]; // sectionのすぐ後ろに入るタスクと判断する if (!prev) return {position: 'previous', id: l(lineNo).id}; const prevEnd = prev.record.end ?? prev.plan.start ?? prev.date; // sectionのすぐ後ろに入るタスクと判断する if (isAfter(sectionDates.start, prevEnd)) return {position: 'previous', id: l(lineNo).id}; } // 見つからなかったら末尾に入れる //console.log(['not found',_]); return {id: l(tasks[tasks.length -1].lineNo).id}; }); //console.log(insertIds.map((id, i) => [sections[i], id.position, l(id.id).text, l(id.id).DOM])); // 見出しを挿入する for (const section of sections) { const id = scrapbox.Page.lines.find(line => line.text === section)?.id; const {position, id: insertId} = insertIds[sections.indexOf(section)]; if (id) { const insertNo = l(insertId).index + (position === 'previous' ? -1 : 0); goLine(id); await sleep(10); moveLinesBefore({from: l(id).index, to: insertNo}); } else { goLine(insertId); await sleep(10); if (position === 'previous') { goHead(); press('Enter'); press('ArrowUp'); } else { press('Enter'); goHead(); selectLine(); } insertText({text: section}); await sleep(1); } } }
繰り返しタスク
タスク名に属性を入れて指定する
タスクの終了時に、次回のタスクを自動生成する
N回分生成する機能もあったほうがいい?
1週間単位で作るときに必要になりそう
これtakker-scheduler-2とは関係ない別の機能として独立させられないかな
流石に無理かtakker
日付を見るところとかガッツリ絡んでるもん
日刊記録sheetの名前を別で分離できないかな
あ、 diaryPage() parseDiary() で分離はできているのか
script.js
export async function transport({targetProject}) { const diaryDate = getPageDate(); // 検索する範囲 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}; // 移動するタスクを取得する // まずタスクを取得する const tasks = scrapbox.Page.lines .slice(startNo, endNo + 1) .flatMap((_, i) => { const task = parse({lineNo: i + startNo}); return task ? [task] : []; }); // 違う日付のタスクはすべて移動する const targetTaskLines = tasks .flatMap(({date, lineNo}) => isValid(diaryDate) && // 日付ページでなければ、全てのタスクを転送する isSameDay(date, diaryDate) ? []: [{date, line: l(lineNo)}] ); // 日付ごとにタスクをまとめる let bodies = {}; targetTaskLines.forEach(({date, line}) => { const key = format(date, 'yyyy-MM-dd'); bodies[key] = { date, texts: [ ...(bodies[key]?.texts ?? []), // indent blockで移動させる ...scrapbox.Page.lines .slice(line.index, line.index + line.indentBlockLength + 1) .map(line => line.text) ], }; }); // 対象の行を削除する for (const {line} of targetTaskLines) { goLine(line.id); await sleep(10); goHead(); press('ArrowLeft'); for(let i = 0; i <= line.indentBlockLength; i++) { press('ArrowRight', {shiftKey: true}); press('End', {shiftKey: true}); } press('Delete'); } // 新しいタブで開く for (const [key, {date, texts}] of Object.entries(bodies)) { const body = encodeURIComponent(texts.join('\n')); window.open(`https://scrapbox.io/${targetProject}/${getDiaryPageTitle(date)}?body=${body}`); } }
やり残したタスクを複製する
script.js
export async function moveIncompleteTasks() { const diaryDate = getPageDate(); if (!diaryDate) return; const now = new Date(); // 検索する範囲 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}; // まずタスクを取得する const tasks = scrapbox.Page.lines .slice(startNo, endNo + 1) .flatMap((_, i) => { const task = parse({lineNo: i + startNo}); return task ? [task] : []; }); // 未完了のタスクを取得する const incompleteTasks = tasks.flatMap(({date, record, lineNo}) => isSameDay(date, diaryDate) && !record.end ? [l(lineNo)] : [] ); // 各タスクに対して移動したことを表す印をつける for (const line of incompleteTasks) { const {date, record, ...rest} = parse({lineNo: line.index}); goLine(line.id); await sleep(10); insertText({text: [ ' moved', ` moved to [${getDiaryPageTitle(now)}]`, create({date: now, ...rest}), ].join('\n')}); await sleep(1); } }

Utilities
script.js
// 全選択 function selectLine() { goHead(); press('End', {shiftKey: true}); } const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
任意のページの日付を取得する
日刊記録sheetでなければ undefined を返す
script.js
function getPageDate(title = undefined) { title = title ?? scrapbox.Page.title; const date = parseDate(title, "'日刊記録sheet' yyyy-MM-dd", new Date()); return isValid(date) ? date : undefined; } // 転送先の日付ページの名前 const getDiaryPageTitle = (date) => format(date, "'日刊記録sheet' yyyy-MM-dd");

実装するかわからないアイデア
タイムラインで視覚化する
SVGでGoogle Calendarっぽいやつを生成する

2022-05-09 17:27:59
#2021-03-14 04:19:02
#2021-03-13 10:39:49
#2021-01-21 16:35:33
#2021-01-16 15:36:44