Closed23

LGTMeow からネコチャン🐈をさっと選んで Pull Request に貼り付けるための Raycast Extension を作る

ピン留めされたアイテム
snakasnaka

LGTMeow の API を叩いて画像を Grid に表示する

APIの結果を元に表示できた

src/neko.tsx
import { useState } from "react";
import { Grid } from "@raycast/api";
import { useFetch } from "@raycast/utils";

type Item = {
  id: number;
  imageUrl: string;
}

export default function Command() {
  const [items, setItems] = useState<Item[]>([]);

  const { isLoading } = useFetch<Item[]>(
    "https://lgtmeow.com/api/lgtm-images",
    {
      onData: (data) => {
        console.log('onData:', data);
        const newItems: Item[] = [];
        for(const item of data) {
          newItems.push(item)
        }
        setItems(newItems);
      }
    }
  );

  return (
    <Grid
      isLoading={isLoading}
      inset={Grid.Inset.Zero}
      filtering={false}
      navigationTitle="Choose a nekochan"
      searchBarPlaceholder=""
    >
      {items.map((item) => (
        <Grid.Item key={item.id} content={item.imageUrl} />
      ))}
    </Grid>
  );
}
snakasnaka

選択したネコチャンのURLを Clipboard にコピーする

クリップボードのAPI

https://developers.raycast.com/api-reference/clipboard

Gridでの選択したものを取得するには onSelectionChange プロパティ

https://developers.raycast.com/api-reference/user-interface/grid

選択したものを Clipboard にコピーする場合は UI コンポーネントとしての Action を使うらしい

src/neco.tsx
      {items.map((item) => (
        <Grid.Item
          id={item.imageUrl}
          key={item.id}
          content={item.imageUrl}
          actions={
            <ActionPanel title="Choose a favorite neko-chan">
              <Action.CopyToClipboard title="Copy URL" content={item.imageUrl} />
            </ActionPanel>
          }
        />
      ))}

https://developers.raycast.com/api-reference/user-interface/action-panel

選択したネコチャンのURLをクリップボードにコピーできた

snakasnaka

選択したネコチャンのURLを Markdown 形式の画像としてコピーできるようにする

本家のコピー形式は

[![LGTMeow](https://lgtm-images.lgtmeow.com/2021/11/16/22/bd2d578a-98ec-45f2-a990-87d30caa10e3.webp)](https://lgtmeow.com)

これに合わせる

snakasnaka

できた

[![LGTMeow](https://lgtm-images.lgtmeow.com/2021/03/16/23/19b15db9-ac6e-4e9c-96ab-8984a2b280ea.webp)](https://lgtmeow.com)

LGTMeow

snakasnaka
src/neko.tsx
              <Action.CopyToClipboard
                title="Copy URL"
                content={formatImageMarkdown(item.imageUrl)}
                onCopy={Clipboard.paste}
              />

これで行けた

snakasnaka

アプリケーションへの貼り付けは別の Action とする

無条件に貼り付けまでされると困る場面もありそうなので、enter は Clipboard へのコピーまでとして、 cmd + enter で貼り付けまで行うように変更する。

https://developers.raycast.com/api-reference/user-interface/action-panel
he first and second action become the primary and secondary action. They automatically get the default keyboard shortcuts assigned. In List, Grid, and Detail, this is ↵ for the primary and ⌘ ↵ for the secondary action.

Action が複数あったときに、デフォルトでそのようなショートカットが割り当てられる

snakasnaka

これで行けた

src/neko.tsx
            <ActionPanel title="Choose a favorite neko-chan">
              <Action.CopyToClipboard
                title="Copy URL"
                content={formatImageMarkdown(item.imageUrl)}
              />
              <Action.CopyToClipboard
                title="Copy URL & Paste to Frontmost App"
                content={formatImageMarkdown(item.imageUrl)}
                onCopy={Clipboard.paste}
              />
            </ActionPanel>

snakasnaka

LGTMeow の一覧画像の取得方法を調べる

https://github.com/nekochans/lgtm-cat-frontend

fetchLgtmImagesUrl(appBaseUrl) でURLを組み立てていそう

https://github.com/nekochans/lgtm-cat-frontend/blob/8fc0b2c18dc19dfbec3c7c95e6a3ed6267989153/src/api/lgtmeow/lgtmImage.ts#L24-L41

https://github.com/nekochans/lgtm-cat-frontend/blob/8fc0b2c18dc19dfbec3c7c95e6a3ed6267989153/src/features/url.ts#L144-L145

挙動見た感じ SSR っぽい?
となると API は公開されてないか

snakasnaka

普通にページリクエストして img 要素抜き出す方針に切り替えてみる

snakasnaka

なんとなくそれっぽいもの

snakasnaka

とりあえず現状のコード

とりあえず動くコード
import { useEffect, useState } from "react";
import { Grid } from "@raycast/api";
import { useFetch } from "@raycast/utils";
import { parseDocument } from "htmlparser2";
import { getElementsByTagName } from "domutils";

const DUMMY_BASE_URL = 'https://example.com';

export default function Command() {
  const [items, setItems] = useState(new Set<string>([]));
  const { isLoading, data } = useFetch<string>("https://lgtmeow.com/");

  useEffect(() => {
    if (!isLoading && data) {
      const dom = parseDocument(data);
      const images = getElementsByTagName("img", dom);

      const newItems = new Set<string>([]);
      for (const image of images) {
        const imgSrc = image.attribs.src;
        const assetUrlParam = new URL(imgSrc, DUMMY_BASE_URL).searchParams.get("url");
        if (assetUrlParam) {
          newItems.add(assetUrlParam);
        }
      }
      setItems(newItems);
    }
  }, [isLoading, data])

  return (
    <Grid
      isLoading={isLoading}
      inset={Grid.Inset.Zero}
      filtering={false}
      navigationTitle="Choose a nekochan"
      searchBarPlaceholder=""
    >
      {[...items].map((image) => (
        console.log("image: ", image),
        <Grid.Item key={image} content={image} />
      ))}
    </Grid>
  );
}

API あったので書き直す

snakasnaka

キャッシュを一定時間経過後に破棄する

どうも useFetch の結果がキャッシュされているっぽいので、一定期間過ぎたらキャッシュ破棄したい
あるいは手動でリロードしたい

snakasnaka

key は options に含まれていないので useFetch に渡すことはできなさそう
revalidate 使うか

snakasnaka

いや、どうやらサーバ側でキャッシュされてそうだな

snakasnaka

あれ?普通に API あった

GET https://lgtmeow.com/api/lgtm-images

でランダムに画像のURLが JSON で取得できる

[
  {
    "id": 35,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2021/03/16/22/e3fe2715-4155-45fc-b1cc-916db866f78f.webp"
  },
  {
    "id": 138,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2021/11/16/22/ae21402b-64a6-4697-bfac-06b9766f75af.webp"
  },
  {
    "id": 522,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2023/08/25/15/9e84fe28-81dc-4394-a9ca-4fb085919352.webp"
  },
  {
    "id": 568,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2023/11/07/09/5a479d0a-83e2-4af3-b266-bf8a8073e6ca.webp"
  },
  {
    "id": 582,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2023/12/11/23/19919a32-e472-4d3a-9074-25b5b5ed32e0.webp"
  },
  {
    "id": 603,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2024/01/23/17/3fb91863-0544-4d7b-8615-3e6327dd14dc.webp"
  },
  {
    "id": 694,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2024/06/25/14/2b95cd5f-4ecc-4812-9e84-f16622235e96.webp"
  },
  {
    "id": 753,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2024/09/26/11/c4805315-3e25-4977-973c-4822e2852027.webp"
  },
  {
    "id": 756,
    "imageUrl": "https://lgtm-images.lgtmeow.com/2024/09/27/15/863abb89-7c10-41d1-b4cf-ef1500a5adc3.webp"
  }
]

このスクラップは2025/01/02にクローズされました