ScrapboxからGyazoのOCR結果を検索するUserScript
仕組み
全文検索すると、ページの一番最後にSearch With Gyazoボタンが表示される
これを押すと、検索文字列が含まれる画像を探し、さらにその画像が埋め込まれているページを探す
originalとの違い
typescriptで書いている
これでだいぶ高速化できているはず
でも予め決められた範囲のprojectを検索するのが限界だよな
↓コードを開くとTamperMonkeyが自動で認識してinstallしてくれる
code:script.user.js
// ==UserScript==
// @name gyazo-search-proxy
// @version 0.1.2
// @description Gyazoの検索機能をScrapboxから使えるようにする
// @author takker
// @grant GM_xmlhttpRequest
// @connect gyazo.com
// @license MIT
// @copyright Copyright (c) 2022 takker
// ==/UserScript==
var r=(e,n)=>new Promise((o,t)=>{GM_xmlhttpRequest({method:c(n?.method),headers:u(n?.headers),url:typeof e=="string"?e:e.url,onload:({responseText:i,status:a,statusText:d})=>{let l=new Response(i,{status:a,statusText:d});o(l)},onerror:i=>t(i)})}),c=e=>{let n=e?.toUpperCase?.();switch(n){case"GET":case"HEAD":case"POST":case void 0:return n;default:return}},u=e=>{if(e===void 0)return e;if(Array.isArray(e))return Object.fromEntries(e);if(e instanceof Headers){let n={};return e.forEach((o,t)=>nt=o),n}return e};var s=async(e,n)=>{let o=https://gyazo.com/api/internal/search_result?page=${n?.page??1}&per=${n?.per??40}&query=${encodeURIComponent(e)},t=await r(o);if(!t.ok)throw Error(${t.status} ${t.statusText});let{captures:i}=await t.json();return i};unsafeWindow.searchGyazo=s; 使ったもの
code:script.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import { searchForPages } from "../scrapbox-userscript-std/rest.ts";
import { toTitleLc, useStatusBar } from "../scrapbox-userscript-std/dom.ts";
import { pool } from "../async-lib/mod.ts";
import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
import type { SearchGyazo } from "./gyazo.ts";
import { makeGyazoArea, makeSearchResult } from "./dom.ts";
declare const scrapbox: Scrapbox;
declare global {
interface Window {
searchGyazo: SearchGyazo;
}
}
export const setup = () => {
launch();
scrapbox.addListener("page:changed", launch);
};
const launch = () => {
if (scrapbox.Layout !== "list") return;
if (!/search\/page/.test(location.href)) return;
const { button, list } = makeGyazoArea();
button.addEventListener("click", async () => {
const { render, dispose } = useStatusBar();
try {
const query = new URLSearchParams(location.search).get("q") ?? "";
render({ type: "spinner" }, {
type: "text",
text: Searching Gyazo for "${query}"...,
});
const images = await window.searchGyazo(query);
const project = scrapbox.Project.name;
const titles = new Set<string>();
await Promise.all([...pool(3, images, async ({ url, image_id }) => {
if (!image_id || !url) return;
const result = await searchForPages(image_id, project);
if (!result.ok) return;
const { pages } = result.value;
for (const { title, lines } of pages) {
if (titles.has(toTitleLc(title))) continue;
titles.add(toTitleLc(title));
render({ type: "spinner" }, {
type: "text",
text:
Searching Gyazo for "${query}"... Found ${titles.size} pages.,
});
list.append(makeSearchResult(project, title, url, lines));
}
})]);
render({ type: "check-circle" }, {
type: "text",
text: Found ${titles.size} pages.,
});
} catch (e) {
if ("name" in e && "message" in e) {
render({ type: "exclamation-triangle" }, {
type: "text",
text: ${e.name} ${e.message},
});
} else {
render({ type: "exclamation-triangle" }, {
type: "text",
text: Unexpected error occurs! (see developer console),
});
}
console.error(e);
} finally {
setTimeout(dispose, 1000);
}
});
};
code:dom.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/** 検索結果表示欄を作る */
export const makeGyazoArea = (): {
button: HTMLButtonElement;
list: HTMLUListElement;
} => {
const area = document.createElement("div");
area.classList.add("project-search");
const count = document.createElement("div");
count.classList.add("project-search-count");
count.textContent = "Search with Gyazo";
area.append(count);
const form = document.createElement("div");
form.classList.add("text-center");
const button = document.createElement("button");
button.id = "gyazo-button";
button.type = "submit";
button.classList.add(
"project-search-button",
"btn",
"btn-auto-block",
"btn-default",
);
button.textContent = "Search with Gyazo";
form.append(button);
area.append(form);
const list = document.createElement("ul");
list.classList.add("list");
list.style.paddingBottom = "15px";
area.append(list);
document.querySelector(".project-search")?.append?.(area);
return { button, list };
};
/** 検索結果を作る */
export const makeSearchResult = (
project: string,
title: string,
imageURL: string,
descriptions: string[],
): HTMLLIElement => {
const item = document.createElement("li");
item.classList.add("page-list-item", "list-style-item");
const a = document.createElement("a");
a.href = /${project}/${encodeURIComponent(title)};
a.rel = "route";
a.style.display = "flex";
a.target = "_blank";
item.append(a);
const imageArea = document.createElement("div");
imageArea.style.paddingRight = "20px";
const image = document.createElement("img");
image.loading = "lazy";
image.src = imageURL;
image.style.width = "250px";
image.style.maxHeight = "400px";
imageArea.append(image);
const summary = document.createElement("div");
summary.style.flex = "1";
const titleDiv = document.createElement("div");
titleDiv.classList.add("title-with-description");
titleDiv.textContent = title;
summary.append(titleDiv);
const description = document.createElement("div");
description.classList.add("description");
for (const line of descriptions) {
const span = document.createElement("span");
span.textContent = line;
description.append(span);
}
summary.append(description);
a.append(imageArea, summary);
return item;
};
TamperMonkey側
code:user.js
import { searchGyazo } from "./gyazo.ts";
unsafeWindow.searchGyazo = searchGyazo;
code:gyazo.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import { fetch } from "./fetch.ts";
export interface Image {
image_id: string;
thumb_url: string;
permalink_url: string | null;
url: string | null;
}
export interface SearchGyazoOptions {
page?: number;
/** 最大検索件数
*
* @default 40
*/
per?: number;
}
export const searchGyazo = async (
query: string,
options?: SearchGyazoOptions,
): Promise<Image[]> => {
options?.page ?? 1
}&per=${options?.per ?? 40}&query=${encodeURIComponent(query)}`;
const res = await fetch(url);
if (!res.ok) throw Error(${res.status} ${res.statusText});
const { captures } = (await res.json()) as { captures: Image[] };
return captures;
};
export type SearchGyazo = typeof searchGyazo;
code:fetch.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import type { Fetch } from "../scrapbox-userscript-std/rest.ts";
export const fetch: Fetch = (input, init) =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: toMethod(init?.method),
headers: toHeaders(init?.headers),
url: typeof input === "string" ? input : input.url,
onload: ({ responseText, status, statusText }) => {
const response = new Response(responseText, {
status,
statusText,
});
resolve(response);
},
onerror: (reason) => reject(reason),
});
});
const toMethod = (
method: string | undefined,
): "GET" | "HEAD" | "POST" | undefined => {
const value = method?.toUpperCase?.();
switch (value) {
case "GET":
case "HEAD":
case "POST":
case undefined:
return value;
default:
return undefined;
}
};
const toHeaders = (
headers: HeadersInit | undefined,
): Tampermonkey.RequestHeaders | undefined => {
if (headers === undefined) return headers;
if (Array.isArray(headers)) {
return Object.fromEntries(headers);
}
if (headers instanceof Headers) {
const record: Record<string, string> = {};
headers.forEach((value, key) => recordkey = value); return record;
}
return headers;
};