generated at
takker-scheduler-3
this is a deprecated script
hr

takker-ScrapSchedulerの実装その3

変更点とか
削除したもの
属性機能
解析がめんどくさい
rep しか使っていなかったのでいらない
rep 任意の日付のタスクを自動生成することで代替する
作業logの作成機能
ページの切り出しで対処するようにした
タスク名 yyyy-MM-dd という名前のページにする
public projectに作業ログを書き出す機能が無いのが面倒
新機能

実装したいこと
2023-11-27 17:25:22 takker-scheduler-3で変えたいところに移動

仕様メモ
2021-03-12
与えられた日付のタスクを生成するか判断する
タスクページに非同期関数を用意する
日付を受け取り、task dataを格納した配列を返す
同日に複数回タスクをやる場合もあるので、配列を返すようにする
生成しない場合は空配列を返す
判断方法は何でもいい
日付のみから単純に判断してもいい
タスクの実行日時が予定よりずれた場合、後からそれを調節する機能が必要?
タスクページの読み込み
特定の文字列をタイトルに含み、かつコードブロックの存在するページのみ読み込む
テスト
その他
task data (takker-scheduler-3)をクラスに包む
操作もまとめた方が扱いやすい気がしてきた
Google Calendarを操作するREST APIは一旦GAS経由で作る
直接rest apiを使おうとすると、認証周りの通信がめんどくさすぎる
GASで操作を仲介させた方が早い
APIの書式はrest apiをパクる
2021-03-11
14:21:17 目的
無計画に時間をむさぼることはもはやできっこない
方針
全てscrapboxで完結させる
時間管理と、忘れても問題ないようにする部分だけ外部ツールにする
表示形式を変える手段として外部ツールを使う
一番簡単なのはGoogle Apps Scriptを使う方法かな
http経由でできるなら、それでもいいけど。
日刊記録sheetの記録をGoogle Calendarに転記する事を思いついた
時間感覚の視覚化ができる
line idを使えば、eventとtritask task lineとの対応付けも簡単だ。
eventのタグに埋め込めばいい。
2021-03-12 01:39:29 これは一旦保留takker
なんか設定が複雑すぎる
欲しい機能?
曖昧すぎ
いったいなにを追跡するというのだ
なにを分析したいのか明確でなければ、実装したところで使わないぞ

2021-07-16
13:04:48 複数の日刊記録sheetとGoogle Calendarを同期できるcommandを作った
12:55:56 takker-scheduler-3/utilsを使うようにした
2021-06-10
2021-05-22
12:59:25 <input> 経由で予定時間を設定する機能を追加した
2021-03-18
08:24:24 log page (takker-scheduler-3)の生成をtakker-scheduler-3/loggerに切り出した
2021-03-15
02:00:03 既存のコードの移植が完了した
sort() だけうまく動いていないが、大した影響ではないので直すのは後にする

directory layout


script.js
import { goHead, goLine, moveRight, } from '../scrapbox-motion-emulation/script.js'; import { insertLine, deleteLines, upBlocks, moveLinesBefore, } from '../scrapbox-edit-emulation-2/script.js'; import {press} from '../scrapbox-keyboard-emulation-2/script.js'; import {position} from '../scrapbox-cursor-position-6/script.js'; import { getLineNo, getLineId, getLine, getCharDOM, } from '../scrapbox-access-nodes@0.1.0/script.js'; import {insertText} from '../scrapbox-insert-text-2/script.js'; import {selection} from '../scrapbox-selection-3/script.js'; import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; import {sleep} from '../sleep/script.js'; import { parse, set, toString, } from '../takker-scheduler-3%2Ftask/script.js'; import {getTitle, getDate} from '../takker-scheduler-3%2Fdiary-page/script.js'; import { lightFormat, parse as parseDate, isValid, isAfter, set as setTime, addDays, subDays, addMinutes, subMinutes, isSameDay, compareAsc, getHours, getMinutes, areIntervalsOverlapping, eachDayOfInterval, } from '../date-fns.min.js/script.js'; import {generatePlan} from '../takker-scheduler-3%2Fplan-generator/script.js'; import {getDatesFromSelection} from '../takker-scheduler-3%2Futils/script.js'; import {syncMultiPages} from '../takker-scheduler-3%2Fsync/bulk.js'; //export {openLogPages} from '../takker-scheduler-3%2Flogger/script.js'; const interval = 5; // 5 minutes export async function addTask() { const lineNo = getLineNo(position().line); const taskLine = parse(lineNo); // 現在行がタスクなら、それと同じ日付にする // 違ったら今日にする const baseDate = taskLine?.baseDate ?? new Date(); // 予定開始時刻と見積もり時刻を計算する // 予定開始時刻は、その前のタスクの見積もり時間にintervalを足したものだけずらしておく const plan = { start: taskLine?.plan?.start ? addMinutes(taskLine.plan.start, interval + (taskLine.plan.duration?.minutes ?? 0)) : undefined, duration: taskLine?.plan?.duration, }; // 書き込む await set({title: '', baseDate, plan, lineNo: lineNo + 1}, {}, {overwrite: false}); } export async function walkDay(count = 1) { if (!selection.exist) { await _walkDay(count); } else { const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range; for (let i = startNo; i <= endNo; i++) { await goLine(i); await _walkDay(count); } } } async function _walkDay(count) { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない await set(taskLine, {baseDate: addDays(taskLine.baseDate, count)}); } export async function moveToday() { if (!selection.exist) { await _moveToday(); } else { const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range; for (let i = startNo; i <= endNo; i++) { await goLine({index: i}); await _moveToday(); } } } async function _moveToday() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const now = new Date(); // 日付に変更がなければ何もしない if (isSameDay(taskLine.baseDate, now)) return; await set(taskLine, {baseDate: now}); } export async function startTask() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない if (taskLine.record?.end) return;// すでに終了していたら何もしない // 開始時刻をtoggleする await set(taskLine, {record: {start: !taskLine.record?.start ? new Date() : 'delete'}}); } export async function endTask() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない if (!taskLine.record?.start) return;// まだ開始していなかったら何もしない // 終了時刻をtoggleする await set(taskLine, {record: {end: !taskLine.record?.end ? new Date() : 'delete'}}); } // 開始→終了→resetを繰り返す export async function toggleTask() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const {title, baseDate, plan, record, lineNo} = taskLine; // 開始していないときは開始する if (!record?.start) { await startTask(); return; } // 終了していないときは終了する if (!record?.end) { await endTask(); return; } // すでに終了しているタスクはリセットする await set({title, baseDate, plan, lineNo}); } export async function posterioriEndTask() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない if (taskLine.record?.start || taskLine.record?.end) return; // 開始していないタスクのみが対象 // 直近のタスクの終了日時を取得する const lineNos = [...scrapbox.Page.lines.keys()] .slice(0, getLineNo(position().line) + 1) .reverse(); let prevEnd = undefined; for (const lineNo of lineNos) { const taskLine_ = parse(lineNo); if (taskLine_?.record?.end) { prevEnd = taskLine_.record.end; break; } } // 上書きする const now = new Date(); await set(taskLine, {record: {start: prevEnd ?? now, end: now}}); } export async function sort() { if (scrapbox.Page.lines.length < 3) return; const startNo = 2; const endNo = scrapbox.Page.lines.length - 1; const sortedTasks = scrapbox.Page.lines .slice(startNo, 1 + endNo) .flatMap((line, i) => { const task = parse(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; await goLine(sortedTasks[i].id); upBlocks(presentPosition - i); } // 見出しもきれいにする insertSections(); } 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 = getDate(); if (!isValid(pageDate)) return; // 前の日付ページへのリンクを挿入する const indexLine = `yesterday: [${getTitle(subDays(pageDate, 1))}]`; if (scrapbox.Page.lines.findIndex(line => line.text === indexLine) !== -1) { const index= scrapbox.Page.lines.findIndex(line => line.text === indexLine); await goLine(index); moveLinesBefore({from: index, to: 0}); } else { await insertLine(1, indexLine); } // 見出しを挿入する // 挿入位置を計算する 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: getLineId(tasks[0])}; // sectionの開始・終了時刻 const sectionDates = { start: setTime(new Date(pageDate), {hours: i * 3, minutes: 0}), end: setTime(new Date(pageDate), {hours: (i + 1) * 3, minutes: 0}), }; // 挿入位置を探す for (const task of tasks) { const {record, plan, baseDate, lineNo} = task; const start = record.start ?? plan.start ?? baseDate; const end = record.end ?? plan.start ?? baseDate; // 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: getLineId(lineNo)}; } else { // sectionのすぐ前に入るタスクと判断する //console.log(['border, previous',_ ,l(lineNo).DOM]); return {position: 'previous', id: getLineId(lineNo)}; } continue; } // あとはどちらか一方にきれいに収まるタスクである場合しか無い // 前のタスクとの間にあるかどうかを調べる if (isAfter(sectionDates.start, start)) continue; const i = tasks.indexOf(task); const prev = tasks[i - 1]; // sectionのすぐ後ろに入るタスクと判断する if (!prev) return {position: 'previous', id: getLineId(lineNo)}; const prevEnd = prev.record.end ?? prev.plan.start ?? prev.date; // sectionのすぐ後ろに入るタスクと判断する if (isAfter(sectionDates.start, prevEnd)) return {position: 'previous', id: getLineId(lineNo)}; } // 見つからなかったら末尾に入れる //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 = getLineNo(insertId) + (position === 'previous' ? -1 : 0); await goLine(id); moveLinesBefore({from: getLineNo(id), to: insertNo}); } else { await goLine(insertId); if (position === 'previous') { goHead(); press('Enter'); press('ArrowUp'); } else { press('Enter'); goHead(); press('End', {shiftKey: true}); } await insertText(section); } } }

script.js
export async function transport({targetProject}) { const diaryDate = getDate(); // 検索する範囲 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 targetTaskLines = scrapbox.Page.lines .slice(startNo, endNo + 1) .flatMap((_, i) => { const taskLine = parse(i + startNo); if (!taskLine) return []; const {baseDate, lineNo} = taskLine; return isValid(diaryDate) && // 日付ページでなければ、全てのタスクを転送する isSameDay(baseDate, diaryDate) ? [] : [{date: baseDate, lineNo}]; }); // 日付ごとにタスクをまとめる let bodies = {}; targetTaskLines.forEach(({date, lineNo}) => { const key = lightFormat(date, 'yyyy-MM-dd'); bodies[key] = { date, texts: [ ...(bodies[key]?.texts ?? []), // indent blockで移動させる ...scrapbox.Page.lines .slice(lineNo, lineNo + getLine(lineNo).lenghth + 1) .map(line => line.text) ], }; }); // 対象の行を削除する await deleteLines(targetTaskLines.map(({line}) => line.id)); // 新しいタブで開く for (const [key, {date, texts}] of Object.entries(bodies)) { const body = encodeURIComponent(texts.join('\n')); window.open(`https://scrapbox.io/${targetProject}/${encodeURIComponent(getTitle(date))}?body=${body}`); } }

予定の作成
script.js
export async function makePlan(count) { await generatePlan([addDays(new Date(), count)], 'takker-memex'); } export async function makeWeekPlan(count) { const now = new Date(); await generatePlan([...array(7).keys()].map(i => addDays(now, i + count)), 'takker-memex'); }

予定の時刻調整
script.js
export async function walkPlanStart(minutes) { if (!selection.exist) { await _walkPlanStart(minutes); } else { const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range; for (let i = startNo; i <= endNo; i++) { await goLine(i); await _walkPlanStart(minutes); } } } async function _walkPlanStart(minutes) { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const date = (taskLine.plan?.start ?? setTime(taskLine.baseDate, {hours: 0, minutes:0})); const newStart = minutes > 0 ? addMinutes(date, minutes) : subMinutes(date, -minutes) await set(taskLine, {plan: {start: newStart, duration: taskLine.plan.duration ?? 0}}); } export async function walkPlanDuration(minutes) { if (!selection.exist) { await _walkPlanDuration(minutes); } else { const {start: {lineNo: startNo}, end: {lineNo: endNo}} = selection.range; for (let i = startNo; i <= endNo; i++) { await goLine(i); await _walkPlanDuration(minutes); } } } async function _walkPlanDuration(minutes) { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const newDuration = {minutes: (taskLine.plan.duration?.minutes ?? 0) + minutes}; await set(taskLine, {plan: {start: taskLine.plan.start, duration: newDuration}}); }

<input type="date">/<input type="time">で入力できるようにしたver.
script.js
import {getValueFromInput} from '../open-input/script.js'; export async function setPlan() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const now = new Date(); const date = (taskLine.plan?.start ?? setTime(taskLine.baseDate, {hours: getHours(now), minutes: getMinutes(now)})); const {top, left} = getChar(postion().line, ' yyyy-MM-dd h'.length - 1).getBoundingClientRect(); const newStart = await getValueFromInput({type: 'time', value: date, x: left, y: top,}); scrapboxDOM.textInput.focus(); await set(taskLine, {plan: {start: newStart, duration: taskLine.plan.duration ?? 0}}); } export async function setDuration() { const taskLine = parse(getLineNo(position().line)); if (!taskLine) return; // タスクでなければ何もしない const {top, left} = getCharDOM(position().line, ' yyyy-MM-dd h'.length - 1) .getBoundingClientRect(); const minutes = await getValueFromInput({type: 'number', value: taskLine.plan.duration?.minutes ?? 0, x: left, y: top, max: 9999, min: 0}); await set(taskLine, {plan: {start: taskLine.plan.start, duration: {minutes}}}); }


選択範囲にある日付に対応する予定を作る
script.js
export async function makePlanFromSelection({minify = false} = {}) { const dates = getDatesFromSelection(); if (dates.length === 0) return; if (dates.length === 1) { await generatePlan([dates[0]], 'takker-memex', {minify}); return; } const [start, end] = dates; await generatePlan( eachDayOfInterval(isAfter(end, start) ? {start, end} : {start: end, end: start}), 'takker-memex', {minify}, ); return; }

選択範囲にある日付に対応する日刊記録sheetとGoogle Calendarを同期する
script.js
export async function syncFromSelection() { const dates = getDatesFromSelection(); if (dates.length === 0) return; if (dates.length === 1) { await syncMultiPages([{project: 'takker-memex', title: getTitle(dates[0])}]); return; } const start = dates[0]; const end = dates.pop(); await syncMultiPages( eachDayOfInterval(isAfter(end, start) ? {start, end} : {start: end, end: start}) .map(date => ({project: 'takker-memex', title: getTitle(date)})) ); }

#2023-11-27 16:36:30
#2022-01-18 12:33:52
#2021-11-06 20:52:20
#2021-07-16 12:43:31
#2021-06-10 15:58:01
#2021-05-22 12:59:49
#2021-05-16 18:00:34
#2021-05-09 13:05:14
#2021-05-02 14:20:51
#2021-03-15 00:11:47
#2021-03-13 00:30:04
#2021-03-12 11:25:52
#2021-02-18 11:57:57