scrapbox-history-slider@0.2.1
使い方
お試し
jsimport('/api/code/programming-notes/scrapbox-history-slider@0.2.1/script.js');
bundle用
preactを既に自分のprojectに入れている人向け
shcurl -o script.js https://scrapbox.io/api/code/programming-notes/scrapbox-history-slider@0.2.1/script.js && esbuild script.js --bundle --minify --format=esm --external:../htm@3.0.4%2Fpreact/script.js --external:../preact@10.5.13/hooks.js --outfile=script.min.js
履歴の日時を付与できるようにした
表示は不完全
UIの配色の調節
実装したいこと
具体的な更新日時を表示したい
<input>
に title
属性をもたせる
脇に更新日時を表示する
sliderに目盛りをつける
1時間前
1日前
1週間前
etc.
読み込みをweb workerに委託する
巨大なページの差分を計算していると時間がかかる
読み込み中表示を詳細に出したい
以下の3項目には最低限分けたい
web workerの用意
頻繁に使う機能ではないので、開くたびに生成・破棄してもperformance上の問題はないだろう
データのfetch
履歴の変換
進捗も出す
行の更新日時ごとに細かく差分を表示できるようにする
2021-07-04
20:21:40 枠のかどを少し丸めて、厚みを増やした
19:43:19 rangeが変更されたときだけsliderの位置をresetする
19:32:19 sliderと履歴の間に区切り線を入れた
空白が空行なのかcomponentの余白なのかがわかりにくかった
19:23:54 枠の色と厚みを変えた
19:19:54 文字色と背景色を直した
19:18:11 行が生成される前の時刻を getSnapshot()
に渡すとエラーになっていた
dependencies
script.jsimport {html, render} from '../htm@3.0.4%2Fpreact/script.js';
import {useState, useRef, useEffect} from '../preact@10.5.13/hooks.js';
import {useLoader} from '../use-loader/script.js';
import {convert} from '../scrapbox-snapshot@0.1.0/script.js';
const App = () => {
const [open, setOpen] = useState(false);
const [max, setMax] = useState(0);
const [index, setIndex] = useState(0);
const [range, setRange] = useState([]);
const [snapshot, setSnapshot] = useState(() => () => '');
const {loading} = useLoader(async () => {
if (!open) return;
const {range, getSnapshot} = await createSnapshot();
setRange(old => JSON.stringify(old) === JSON.stringify(range) ? old : range);
setSnapshot(() => getSnapshot);
}, {delay: 1000}, [open]);
// rangeが変更されたときだけsliderの位置をresetする
useEffect(() => {
setMax(range.length - 1);
setIndex(range.length - 1);
}, [range]);
useEffect(() => {
scrapbox.PageMenu.addMenu({
title: '履歴スライダー',
image: 'https://1.bp.blogspot.com/-UZtkSEX0wh4/U5l5_dNcEsI/AAAAAAAAhWs/UzJGVzyiX8Y/s800/kaichu_dokei.png',
onClick: () => setOpen(true),
});
}, []);
const close = () => setOpen(false);
const onSliderChange = ({target: {value}}) => setIndex(parseInt(value));
return html`
<style>
:host {
color: var(--page-text-color, #4a4a4a);
}
.background {
position: fixed;
top: 0;
right: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
z-index: 89999;
}
.container {
position: fixed;
top: 5px;
left: 5px;
width: calc(100% - 10px);
max-height: 80vh;
background-color: var(--page-bg, #fefefe);
border-radius: 4px;
border: 2px solid var(--body-bg, #dcdde0);
z-index: 90000;
}
.close-button {
font-size: 30px;
line-height: 1em;
padding: 0;
width: 30px;
height: 30px;
}
pre {
width: 100%;
max-height: 70vh; /* なぜか100%だとはみ出てしまった */
overflow-y: auto;
font-family: var(--history-slider-pre-font, Menlo,Monaco,Consolas,"Courier New",monospace);
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
}
input {
width: 90%;
}
</style>
${open && html`
<div class="background" onClick="${close}"/>
<div class="container">
<div style="display: inline">
<input type="range" max="${max}" min="0" step="1" value="${index}"
title="${new Date(range[index] * 1000)}"
onInput="${onSliderChange}" />
<button class="close-button" onClick="${close}">x</button>
</div>
<hr />
${loading && 'Loading...'}
<pre>${snapshot(range[index])}</pre>
</div>
`}
`;
};
const app = document.createElement('div');
app.dataset.userscriptName= 'history-slider';
app.attachShadow({mode: 'open'});
document.body.append(app);
render(html`<${App} />`, app.shadowRoot);
履歴を作成する
script.jsasync function createSnapshot() {
const [commitHistory, pageHistory] = await Promise.all([getCommitsHistory(), getPageSnapshots()]);
const getSnapshot = time => {
// 範囲外ならpageHistoryから取得する
if (!commitHistory.range.includes(time)) {
if (!pageHistory.range.includes(time)) return '';
return pageHistory.pages[time].map(line => line.text).join('\n');
}
return commitHistory.history.flatMap(({id, snapshots}) => {
// timeの時の文字列を取得する
const times = Object.keys(snapshots).map(key => parseInt(key));
const key = times.includes(time) ? time : times.filter(key => key <= time).pop();
// timeの時まだ行が生成されていなければundefinedになる
if (key === undefined) return [];
const line = snapshots[key];
return line.text !== null ? [line.text] : [];
}).join('\n');
}
return {
range: [...pageHistory.range, ...commitHistory.range],
getSnapshot,
};
}
async function getPageSnapshots() {
const res = await fetch(`/api/page-snapshots/${scrapbox.Project.name}/${scrapbox.Page.id}`);
const {snapshots} = await res.json();
const pages = Object.fromEntries(snapshots.map(({lines, created}) => [created, lines]));
const range = Object.keys(pages).map(created => parseInt(created)).sort();
return {range, pages};
}
async function getCommitsHistory() {
const res = await fetch(`/api/commits/${scrapbox.Project.name}/${scrapbox.Page.id}`);
const {commits} = await res.json();
return convert(commits);
}