personal-pin
全体には反映されず、自分にだけ反映される
プロジェクト全体に反映されるのではなく、自分にしか反映されないピン留め
リマインドにも使えそう
もしこれができるようになったら、@ページをピン留めしたい…
と思ったけれど@ページ関しては、いつでもアクセスしたいわけじゃなくて通知を受け取りたいだけだから違うか
仮にピン留めしたとしても新着に気付けるわけではない
それなら未読のときだけpinされるようにすればいいな
未読検出は簡単
天才
使用例:未読の@ページをピンする
unreadDM.jsimport { launch } from "./mod.js";
const title = "@Mijinko_SD";
launch(async () => {
const res = await fetch(`/api/pages/villagepump/${title}`);
if (!res.ok) return [];
const { persistent, lastAccessed, updated } = await res.json();
return persistent && lastAccessed < updated ? [title] : [];
});
2022-11-18 これ動いていますか?というかそもそも使っている人いますか?
自分は使っていないので、まともに動作しているのかわからない
1箇所修正すれば動作しました
修正ありがとうございます!
名付けてみたがなんか微妙
あらあら
別の名前にしよう
いい感じの名前かいてけ
秘密ピン
こっそりピン留め
あとで読む
安直かな
あとで読む以外の使い道もあるのでびみょい
myMyピン止め
機能の特徴が簡潔に書かれていてわかりやすいと思う
個人的にはmは大文字の方が好み
大文字にした
2022-06-14
18:43:17 できた
bundleなしで、そのまま実行できるようにした
型チェックはしたけど動作は未検証
試してみてください
コアモジュール
launch
にピン止めしたいページを渡して実行する
titles
にページのタイトルのリストを渡す
前から順にピン止めされる
函数を渡すこともできる
更新するたびにピンするページを変えたいときに使う
asyncがついた函数も渡せる
函数を渡した場合、その函数はページ名が入った配列を返さなければならない
iteratorでも可
arrow functionsにしたり型をちゃんと書いたり不要な函数を削ったりしている
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/villagepump/personal-pin/mod.js
mod.js// @ts-check
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/// <reference types="/api/code/villagepump/personal-pin%2F@types/mod.d.ts" />
/**
* @typedef {import("https://raw.githubusercontent.com/scrapbox-jp/types/0.3.5/scrapbox.ts").Scrapbox} Scrapbox
*/
/** @type {Scrapbox} */
// @ts-ignore declare使えないので無理やり色々やっている
const scrapbox = window.scrapbox;
/** @typedef {object} LaunchOptions
* @property {string} [project] ピン留めするプロジェクト名 既定は現在のプロジェクト名
* @property {number} [interval=60000] ピン留め更新処理の間隔
*/
/** ピン留め処理を開始する
*
* @param {Iterable<string>|(()=>(Iterable<string>|Promise<Iterable<string>>))} titles ピン留めしたいページのタイトル
* @param {LaunchOptions} [options] オプション
* @return {()=>void} 後始末用函数
*/
export const launch = (titles, options) => {
const { project = scrapbox.Project.name, interval = 60000 } = options ?? {};
/** @type {number | undefined} */
let updateTimer;
const end = () => clearInterval(updateTimer);
const start = async () => {
end();
await updatePins(project, titles);
updateTimer = setInterval(() => updatePins(project, titles), interval);
};
const handleChange = () =>
// トップページでのみ起動する
scrapbox.Layout === "list" && !location.pathname.startsWith("/stream") &&
scrapbox.Project.name === project
ここ
!==
じゃなくて
===
かも ↑
ほんとだ
直しました
mod.js ? start()
: end();
handleChange();
scrapbox.addListener("layout:changed", handleChange);
//トップページからトップページに移動してもlayout:changedは呼ばれないので、ここでhandleChangeをかける
scrapbox.addListener("project:changed", handleChange);
return () => {
end();
scrapbox.removeListener("layout:changed", handleChange);
scrapbox.removeListener("project:changed", handleChange);
};
};
/** ピン留め処理
*
* @param {string} project プロジェクト名
* @param {Iterable<string>|(()=>(Iterable<string>|Promise<Iterable<string>>))} titles ピン留めしたいページのタイトル
*/
const updatePins = async (project, titles) => {
/** タイトルのリスト
*
* `titles`の型を調整したもの
*
* @type {Iterable<string>}
*/
let list;
if (typeof titles === "function") {
const result = titles();
list = result instanceof Promise ? await result : result;
} else {
list = titles;
}
// ピン留めカードを一旦全部消してから作り直す
deletePseudoCards();
for (const title of list) {
const card = makePseudoCard(project, title);
// ピン留め済みなら何もしない
if (card.classList.contains("pin")) continue;
const pins = listPins();
pin(card);
if (pins.length > 0) {
const lastPin = pins[pins.length - 1];
lastPin.parentElement?.insertBefore?.(card, lastPin);
} else {
cardList()?.insertAdjacentElement?.("afterbegin", card);
}
}
};
/** 指定したカードをpinする
*
* @param {HTMLLIElement} card
* @return {void}
*/
const pin = (card) => {
card.getElementsByClassName("hover")?.[0]
?.insertAdjacentHTML("afterend", '<div class="pin"></div>');
card.classList.add("pin");
};
/** トップページのカードリストを取得する */
const cardList = () => {
const cards = document.getElementsByClassName("page-list")?.[0]
?.getElementsByClassName(
"grid",
)?.[0];
if (!cards) return undefined;
if (!(cards instanceof HTMLUListElement)) {
throw TypeError('".page-list .grid" must be ul.grid');
}
return cards;
};
/** ピン留めページを取得する */
const listPins = () =>
Array.from(
cardList()?.getElementsByClassName?.(
"page-list-item grid-style-item pin",
) ?? [],
);
const id = "personal-pin-card";
/** ページカードを作る
*
* ページリストにあるときはそれをコピーし、ないときは空のページカードを作る
* @param {string} project プロジェクト名
* @param {string} title ページタイトル
* @return {HTMLLIElement}
*/
const makePseudoCard = (project, title) => {
const path = `/${project}/${encodeTitleURI(title)}`;
const card = cardList()?.querySelector?.(
`li.page-list-item a[href="${path}"]`,
)?.parentNode;
if (!card) {
const card = document.createElement("li");
card.dataset.id = id;
card.classList.add("page-list-item", "grid-style-item");
card.style.opacity = "0.7";
card.insertAdjacentHTML(
"beforeend",
`<a href="/${scrapbox.Project.name}/${
encodeURIComponent(title)
}" rel="route">
<div class="hover"></div>
<div class="content">
<div class="header">
<div class="title">${title}</div>
</div>
<div class="description">
<div class="line-img">
<div></div><div></div><div></div><div></div><div></div>
</div>
</div>
</div>
</a>`,
);
return card;
}
const cloned = card.cloneNode(true);
if (!(cloned instanceof HTMLLIElement)) {
throw TypeError('"li.page-list-item" must be ul.grid');
}
cloned.dataset.id = id;
return cloned;
};
/** このscriptで作成したカードを全部消す */
const deletePseudoCards = () => {
document.querySelectorAll(`li[data-id="${id}"]`)
.forEach((card) => card.remove());
};
const noEncodeChars = '@$&+=:;",';
const noTailChars = ':;",';
/** titleをURIで使える形式にEncodeする
*
* ported from https://github.com/takker99/scrapbox-userscript-std/blob/0.14.5/title.ts#L28
*
* @param {string} title 変換するtitle
* @return {string} 変換後の文字列
*/
const encodeTitleURI = (title) => {
return [...title].map((char, index) => {
if (char === " ") return "_";
if (
!noEncodeChars.includes(char) ||
(index === title.length - 1 && noTailChars.includes(char))
) {
return encodeURIComponent(char);
}
return char;
}).join("");
};