personal-pin
全体には反映されず、自分にだけ反映される
ピン留めの個人版Mijinko_SD.iconnishio.icon プロジェクト全体に反映されるのではなく、自分にしか反映されないピン留め
なるほど、これがあれば個人projectの自分用ピン留めと来訪者用ピン留めができるかnishio.icon リマインドにも使えそうtakker.icon
もしこれができるようになったら、@ページをピン留めしたい…Mijinko_SD.icon
と思ったけれど@ページ関しては、いつでもアクセスしたいわけじゃなくて通知を受け取りたいだけだから違うか
仮にピン留めしたとしても新着に気付けるわけではない
それなら未読のときだけpinされるようにすればいいなtakker.icon
未読検出は簡単
天才Mijinko_SD.icon
使用例:未読の@ページをピンする
code:unreadDM.js
import { 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 これ動いていますか?というかそもそも使っている人いますか?takker.icon
自分は使っていないので、まともに動作しているのかわからない
1箇所修正すれば動作しましたMijinko_SD.icon
修正ありがとうございます!takker.icon
名付けてみたがなんか微妙takker.icon
あらあらtakker.icon
別の名前にしよう
いい感じの名前かいてけ
秘密ピン
こっそりピン留め
あとで読む
安直かなwogikaze.icon
あとで読む以外の使い道もあるのでびみょいtakker.icon
myMyピン止め
機能の特徴が簡潔に書かれていてわかりやすいと思うMijinko_SD.icon
個人的にはmは大文字の方が好みMijinko_SD.icon
大文字にしたtakker.icon
2022-06-14
18:43:17 できたtakker.icon
bundleなしで、そのまま実行できるようにした
型チェックはしたけど動作は未検証
試してみてください
コアモジュール
launchにピン止めしたいページを渡して実行する
titlesにページのタイトルのリストを渡す
前から順にピン止めされる
函数を渡すこともできる
更新するたびにピンするページを変えたいときに使う
asyncがついた函数も渡せるMijinko_SD.icon
函数を渡した場合、その函数はページ名が入った配列を返さなければならないMijinko_SD.icon
iteratorでも可takker.icon
arrow functionsにしたり型をちゃんと書いたり不要な函数を削ったりしている
code: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" />
/**
*/
/** @type {Scrapbox} */
// @ts-ignore declare使えないので無理やり色々やっている
const scrapbox = window.scrapbox;
/** @typedef {object} LaunchOptions
* @property {string} project ピン留めするプロジェクト名 既定は現在のプロジェクト名 */
/** ピン留め処理を開始する
*
* @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
ここ!==じゃなくて===かも ↑ Mijinko_SD.icon
ほんとだtakker.icon
直しました
code: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) {
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",
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する
*
*
* @param {string} title 変換するtitle
* @return {string} 変換後の文字列
*/
const encodeTitleURI = (title) => {
if (char === " ") return "_";
if (
!noEncodeChars.includes(char) ||
(index === title.length - 1 && noTailChars.includes(char))
) {
return encodeURIComponent(char);
}
return char;
}).join("");
};