generated at
次に取るべき行動の一覧を表示するPage Menu
次に取るべき行動の一覧を表示するPageMenu
メモ帳を表示するPage Menuをベースに作った

機能
/^(?:⬜(?:[^p]*|p[^-]*))|🔳/ (次に取るべき行動 (GTD)の識別子)にマッチするリンクを/takker-memex/takkerから探して表示する
押すとそのページへ飛ぶ
同じprojectの場合は同じページで開く
違うprojectの場合は新しいタブで開く
実装
更新機能は削る
そのうち実装するかも
今開いているprojectはscrapbox.Project.pagesから、別なprojectの場合はapi/pages/:projectname/search/titlesからリンクデータを取得する

実装したいこと
takker-workflow@0.0.1に沿ったやつにしたい
このUserScirptに対して以下の不満がある
全てのやることがでてきても嬉しくない
今日やるタスクや、1週間以内にやるタスクなどに絞って閲覧したい
🚧などがでてこない
このUserScriptはあくまで試しに作ってみた程度だから、これは別のuserscriptとして改めて作るべきかな

script.js
import { setup } from "./mod.js"; setup(["takker-memex", "takker"]);

mod.js
var l=(e,t)=>{if(!(e instanceof HTMLDivElement))throw new TypeError(`"${t}" must be HTMLDivElememt but actual is "${e}"`)};var m=()=>w(document.getElementsByClassName("status-bar")?.[0],"div.status-bar"),w=(e,t)=>{if(!!e)return l(e,t),e};function p(){let e=m();if(!e)throw new Error("div.status-bar can't be found");let t=document.createElement("div");return e.append(t),{render:(...n)=>{t.textContent="";let r=g(...n);r&&t.append(r)},dispose:()=>t.remove()}}function g(...e){let t=e.flatMap(r=>{switch(r.type){case"spinner":return[k()];case"check-circle":return[C()];case"exclamation-triangle":return[v()];case"text":return[a(r.text)];case"group":{let o=g(...r.items);return o?[o]:[]}}});if(t.length===0)return;if(t.length===1)return t[0];let n=document.createElement("span");return n.classList.add("item-group"),n.append(...t),n}function a(e){let t=document.createElement("span");return t.classList.add("item"),t.append(e),t}function k(){let e=document.createElement("i");return e.classList.add("fa","fa-spinner"),a(e)}function C(){let e=document.createElement("i");return e.classList.add("kamon","kamon-check-circle"),a(e)}function v(){let e=document.createElement("i");return e.classList.add("fas","fa-exclamation-triangle"),a(e)}var u=e=>[...e].map((t,n)=>t===" "?"_":!D.includes(t)||n===e.length-1&&I.includes(t)?encodeURIComponent(t):t).join(""),D='@$&+=:;",',I=':;",';function x(e,t,n){let r=document.createElement("a");r.href=`/${e}/${u(t)}${typeof n!="string"?"":`?body=${encodeURIComponent(n)}`}`,document.body.append(r),r.click(),r.remove()}var i="next-action",h,A="/assets/img/favicon/apple-touch-icon.png";function rt(e){let t=`head style[data-userscript-name="${i}"]`;document.querySelector(t)?.remove?.();let n=document.createElement("style");n.dataset.userscriptName=i,n.textContent=`a#${i}.tool-btn:hover { text-decoration: none; } a#${i}.tool-btn::before { position: absolute; content: "\\f0ae"; font: 900 20px/46px "Font Awesome 5 Free"; } a#${i}.tool-btn img { opacity: 0; } a#${i}.tool-btn ~ ul a::before { position: absolute; font-family: "Font Awesome 5 Free"; font-weight: 900; } a#${i}.tool-btn ~ ul img { opacity: 0; margin-right: 0; }`,document.head.append(n),document.getElementById(i)||scrapbox.PageMenu.addMenu({title:i,image:A,onClick:async()=>{h??=B(e),await h}})}async function B(e){scrapbox.PageMenu(i).removeAllItems();let{render:t,dispose:n}=p(),r=0;try{for(let o of e){t({type:"spinner"},{type:"text",text:`Searching "/${o}" for next actions...`});for await(let s of K(o))r++,scrapbox.PageMenu(i).addItem({title:s,onClick:()=>{let c=`https://scrapbox.io/${o}/${u(s)}`;if(o!==scrapbox.Project.name){window.open(c);return}x(c)}});o!==e[e.length-1]&&scrapbox.PageMenu(i).addSeparator()}t({type:"check-circle"},{type:"text",text:`Found ${r} actions.`})}catch(o){t({type:"exclamation-triangle"},{type:"text",text:o instanceof Error?`${o.name} ${o.message}`:"Unknown error! (see developper console)"}),console.error(o)}finally{setTimeout(()=>n(),1e3)}}async function*K(e,t){if(t??=/^(?:⬜(?:[^p]*|p[^-]*))|🔳/,e===scrapbox.Project.name){for(let{title:r,exists:o}of scrapbox.Project.pages)!t.test(r)||(yield r);return}let n=new Set;for await(let r of N(e))!n.has(r)&&t.test(r)&&(n.add(r),yield r)}async function*N(e){let t=`/api/pages/${e}/search/titles`,n=null;do{let r=`${t}${n?`?followingId=${n}`:""}`,o=await fetch(r);n=o.headers.get("X-following-id");let s=await o.json();for(let{title:c,links:E}of s){yield c;for(let y of E)yield y}if(!n)break}while(!0)}export{rt as setup};


mod.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> import { useStatusBar, openInTheSameTab, encodeTitleURI, } from "../scrapbox-userscript-std/dom.ts"; import type { Scrapbox, SearchedTitle } from "https://raw.githubusercontent.com/scrapbox-jp/types/0.0.8/mod.ts"; declare const scrapbox: Scrapbox; const id = "next-action"; let initialized: Promise<void>; const dummyImage = "/assets/img/favicon/apple-touch-icon.png"; export function setup(projects: string[]) { const selector = `head style[data-userscript-name="${id}"]`; document.querySelector(selector)?.remove?.(); const style = document.createElement("style"); style.dataset.userscriptName = id; style.textContent = `a#${id}.tool-btn:hover { text-decoration: none; } a#${id}.tool-btn::before { position: absolute; content: "\\f0ae"; font: 900 20px/46px "Font Awesome 5 Free"; } a#${id}.tool-btn img { opacity: 0; } a#${id}.tool-btn ~ ul a::before { position: absolute; font-family: "Font Awesome 5 Free"; font-weight: 900; } a#${id}.tool-btn ~ ul img { opacity: 0; margin-right: 0; }`; document.head.append(style); if (!document.getElementById(id)) { scrapbox.PageMenu.addMenu({ title: id, image: dummyImage, onClick: async () => { initialized ??= load(projects); await initialized; }, }); } }

mod.ts
async function load(projects: string[]) { scrapbox.PageMenu(id).removeAllItems(); const { render, dispose } = useStatusBar(); let count = 0; try { for (const project of projects) { render( { type: "spinner" }, { type: "text", text: `Searching "/${project}" for next actions...`}, ); for await (const title of listNextActions(project)) { count++; scrapbox.PageMenu(id).addItem({ title, onClick: () => { const path = `https://scrapbox.io/${ project }/${encodeTitleURI(title)}`; if (project !== scrapbox.Project.name) { window.open(path); return; } openInTheSameTab(path); }, }); } if (project === projects[projects.length - 1]) continue; scrapbox.PageMenu(id).addSeparator(); } render( { type: "check-circle" }, { type: "text", text: `Found ${count} actions.`}, ); } catch(e: unknown) { render( { type: "exclamation-triangle" }, { type: "text", text: e instanceof Error ? `${e.name} ${e.message}` : `Unknown error! (see developper console)`, }, ); console.error(e); } finally { setTimeout(() => dispose(), 1000); } }

mod.ts
async function* listNextActions(project: string, filter?: RegExp) { filter ??= /^(?:⬜(?:[^p]*|p[^-]*))|🔳/; if (project === scrapbox.Project.name) { for (const { title, exists } of scrapbox.Project.pages) { if (!filter.test(title)) continue; yield title; } return; } const titles = new Set<string>(); for await (const title of getLinks(project)) { if (!titles.has(title) && filter.test(title)) { titles.add(title); yield title; } } } async function* getLinks(project: string) { const path = `/api/pages/${project}/search/titles`; let followingId = null; do { const path_ = `${path}${ followingId ? `?followingId=${followingId}` : "" }` as string; const res = await fetch(path_); followingId = res.headers.get("X-following-id"); const pages = (await res.json()) as SearchedTitle[]; for (const { title, links } of pages) { yield title; for (const link of links) { yield link; } } if (!followingId) break; // 空文字列の場合もある } while (true) }

#2022-02-21 15:13:03