作業時間を🍅でリアルタイムに表示するUserScript
2025-05-13
15:53:03 コードミスに気づく
完了していないタスクがなければ、完了したタスクの終了日時で一番遅いものをstartとする
16:03:53 修正完了
15:50:57 cacheから取ってくる機能も入れた
15:40:40 実装できた。問題なさそう
https://gyazo.com/f4173f612cd600e1e5a2cf3b5540d002
1分ごとに更新する
リアルタイム性はないが、1分間隔なら十分実用的
1分ごとにnetworkを叩いている
全てのタブで一斉にnetworkを叩くことになる
1分未満でcacheがあったときは、そちらを優先するコードにしたいかも
2025-05-11 21:18:10 全てのページで、現在開始時刻からどのくらい時間が経過したか見れるようにしたい
2025-04-16 13:35:00 🍊と🍓を追加 13:50:14 よさそう
https://gyazo.com/4c4460c1a33857e56c07bca36aa26e5c
2025-02-06 22:01:47 未完了の作業は、1分ごとに🍅の数を更新する
percent encodingしないと認識されない
code:main.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
import { getLineDOM, takeInternalLines, useStatusBar, findLatestCache } from "jsr:@cosense/std@0.29/browser/dom";
import { get } from "jsr:@cosense/std@0.29/unstable-api/pages/project/title";
import type { BaseLine, Scrapbox } from "jsr:@cosense/types@0.10/userscript";
import { parse } from "../../takker/takker99%2Ftakker-scheduler/deps.ts";
import { toTitle } from "../../takker/takker99%2Ftakker-scheduler/diary.ts";
import { differenceInMinutes } from "npm:date-fns@4/differenceInMinutes";
import { differenceInSeconds } from "npm:date-fns@4/differenceInSeconds";
import { subDays } from "npm:date-fns@4/subDays";
import { startOfDay } from "npm:date-fns@4/startOfDay";
import { max } from "npm:date-fns@4/max";
import { makeIndicator } from "./make_indicator.ts";
declare const scrapbox: Scrapbox;
const makeTomatoDom = (): HTMLDivElement => {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.top = "0";
div.style.right = "0";
div.style.textAlign = "right";
return div
};
const tomatoDoms = new Map<string, HTMLDivElement>();
let timer: number | undefined;
const updateTomato = (): void => {
for (const tomatoDom of tomatoDoms.values()) {
tomatoDom.remove();
}
tomatoDoms.clear();
const callback = () => checkLines(takeInternalLines());
if (scrapbox.Layout !== "page") {
scrapbox.removeListener("lines:changed", callback);
if (!timer) return;
clearInterval(timer);
timer = undefined;
return;
}
callback();
scrapbox.addListener("lines:changed", callback);
timer = setInterval(callback, 60 * 1000);
};
const checkLines = (lines: readonly BaseLine[]): void => {
for (const line of lines) {
const task = parse(line.text);
if (!task?.record?.start) {
const tomatoDom = tomatoDoms.get(line.id);
tomatoDom?.remove?.();
tomatoDoms.delete(line.id);
continue;
}
const tomatoDom = tomatoDoms.get(line.id) ?? makeTomatoDom();
getLineDOM(line.id)?.append?.(tomatoDom);
tomatoDoms.set(line.id, tomatoDom);
tomatoDom.textContent = makeIndicator(
differenceInMinutes(task.record.end ?? new Date(), task.record.start)
);
}
};
if (scrapbox.Project.name === "takker-memex") {
updateTomato();
scrapbox.addListener("page:changed", updateTomato);
scrapbox.addListener("lines:changed", updateTomato);
}
const getUncompleteTasksStartDate = (lines: readonly BaseLine[]): Date | undefined => {
const task = parse(text);
if (!task?.record?.start) return acc;
if (task.record.end) {
acc1.push(task.record.end); return acc;
}
if (task.record.start) acc0.push(task.record.start); return acc;
}, ], [ as [Date[], Date[]]);
if (starts.length === 0) starts.push(...ends);
if (starts.length === 0) return;
return max(starts);
};
const getRecentDiaryLines = async (project: string, date: Date): Promise<BaseLine[]> => {
get(project, toTitle(date), { fetch: useCacheInMinutes }),
get(project, toTitle(subDays(date, 1)), { fetch: useCacheInMinutes }),
]);
const lines = todayRes.ok ? (await todayRes.json()).lines : [];
if (yesterdayRes.ok) {
lines.push(...(await yesterdayRes.json()).lines);
}
return lines;
};
/** 1分未満にfetchしたcacheがあればそちらを使う */
const useCacheInMinutes: typeof globalThis.fetch = async (req, init) => {
const cache = await findLatestCache(new Request(req, init));
return !cache || differenceInSeconds(
new Date(),
new Date(${cache.headers.get("Date")}),
) >= 60 ? globalThis.fetch(req, init) : cache;
};
const { render } = useStatusBar();
const renderUncompleteIndicator = async (): Promise<void> => {
const start = getUncompleteTasksStartDate(
await getRecentDiaryLines("takker-memex", new Date())
) ?? startOfDay(new Date());
render({
type: "text",
text: makeIndicator(differenceInMinutes(new Date(), start)),
});
};
renderUncompleteIndicator();
setInterval(renderUncompleteIndicator, 60 * 1000);
code:make_indicator.ts
/**
* 所要時間を🍅🍊🍓に変換する
*
* @param duration in minutes
*/
export const makeIndicator = (duration: number): string => {
const unitCount = Math.round(duration / 5);
const tomatoCount = Math.floor(unitCount / 6);
const satsumaCount = Math.floor((unitCount - tomatoCount * 6) / 3)
const strawberryCount = unitCount - tomatoCount * 6 - satsumaCount * 3;
return `${
// maximum is 🍅x20
tomatoCount > 20
? 🍅x${(duration / 30).toFixed(1)}
: "🍅".repeat(tomatoCount)
}${"🍊".repeat(satsumaCount)}${"🍓".repeat(strawberryCount)}`;
};