Ada Eklentileri

Örnekler

Baştan sona çalışan dört eklenti: hava durumu, medya kontrolleri, pomodoro ve GitHub PR'ları.

Her örnek adanotch init ile oluşturulmuş bir klasörde manifest.json ve src/index.ts olarak çalışır. Hepsi Adanotch'un test kitiyle denenmiştir.

Hava durumu

fetch, ayarlar, çevrimdışıyken son iyi yanıt ve grafik. Anahtar gerektirmeyen Open-Meteo kullanır.

const { activity, ui } = AdanotchKit;

interface Settings {
  latitude: number;
  longitude: number;
  city: string;
}

interface Forecast {
  current: { temperature_2m: number; weather_code: number };
  daily: { time: string[]; temperature_2m_max: number[]; temperature_2m_min: number[] };
}

function symbol(code: number): string {
  if (code === 0) return "sun.max.fill";
  if (code <= 3) return "cloud.sun.fill";
  if (code <= 48) return "cloud.fog.fill";
  if (code <= 67) return "cloud.rain.fill";
  if (code <= 77) return "cloud.snow.fill";
  return "cloud.bolt.rain.fill";
}

activity<Settings>({
  async update(ctx) {
    const url = new URL("https://api.open-meteo.com/v1/forecast");
    url.searchParams.set("latitude", String(ctx.settings.latitude));
    url.searchParams.set("longitude", String(ctx.settings.longitude));
    url.searchParams.set("current", "temperature_2m,weather_code");
    url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min");
    url.searchParams.set("timezone", "auto");

    let data: Forecast;
    let stale = false;
    try {
      data = await ctx.network.fetchJSON<Forecast>(url.toString());
      ctx.storage.set("last", data);
    } catch (error) {
      const last = ctx.storage.get<Forecast>("last");
      if (!last || !(error instanceof NetworkError || error instanceof TimeoutError)) throw error;
      data = last;
      stale = true;
    }

    const temp = Math.round(data.current.temperature_2m);
    const highs = data.daily.temperature_2m_max;
    return {
      compact: {
        leading: ui.icon(symbol(data.current.weather_code)),
        trailing: ui.text(`${temp}°`, { tone: stale ? "secondary" : "default" }),
      },
      expanded: ui.stack([
        ui.stack(
          [
            ui.icon(symbol(data.current.weather_code), { tone: "accent" }),
            ui.text(ctx.settings.city, { style: "title" }),
            ui.spacer(),
            ui.text(`${temp}°`, { style: "display" }),
          ],
          { axis: "h", spacing: 8, align: "center" },
        ),
        ui.chart(highs, { style: "bar", tone: "warning" }),
        stale && ui.text("Çevrimdışı; son bilinen değer", { style: "caption", tone: "secondary" }),
      ]),
    };
  },
});

Şu an çalan

Olaylarla güncellenen bir eklenti: media.change, medya kontrolleri ve refresh: manual. Adanotch'un Müzik modülü açık olmalı.

const { activity, ui, media, events } = AdanotchKit;

activity({
  setup(ctx) {
    events.on("media.change", () => ctx.refresh());
  },

  async update() {
    const track = await media.nowPlaying();
    if (!track) {
      return { compact: null, expanded: ui.text("Şu an bir şey çalmıyor", { tone: "secondary" }) };
    }
    const percent = track.duration > 0 ? (track.elapsed / track.duration) * 100 : 0;
    return {
      compact: {
        leading: ui.icon(track.isPlaying ? "waveform" : "pause.fill", { tone: "accent" }),
        trailing: ui.progress(percent, { tone: "accent" }),
      },
      expanded: ui.stack([
        ui.text(track.title, { style: "title" }),
        ui.text(`${track.artist} · ${track.album}`, { tone: "secondary" }),
        ui.progress(percent),
        ui.stack(
          [
            ui.button("", "previous", { icon: "backward.fill" }),
            ui.button("", "toggle", { icon: track.isPlaying ? "pause.fill" : "play.fill", primary: true }),
            ui.button("", "next", { icon: "forward.fill" }),
            ui.spacer(),
            ui.button("+30 sn", "seek", { value: Math.min(track.elapsed + 30, track.duration) }),
          ],
          { axis: "h", spacing: 8 },
        ),
      ]),
    };
  },

  async onAction(action, _ctx, value) {
    if (action === "previous") await media.previous();
    if (action === "toggle") await media.toggle();
    if (action === "next") await media.next();
    if (action === "seek") await media.seek(Number(value));
  },
});

Pomodoro

Zamanlayıcılar, kompakt alanda kendi sayan ui.timer, ada bildirimleri, kalıcı durum ve Kestirmeler komutları (adanotch ext local.sen.pomodoro start).

const { activity, ui, events, island, notify } = AdanotchKit;

interface Settings {
  focusMinutes: number;
  breakMinutes: number;
}

type Phase = "idle" | "focus" | "break";
interface State {
  phase: Phase;
  endsAt: number | null;
  done: number;
}

let timer: number | undefined;

activity<Settings>({
  setup(ctx) {
    events.on("command", ({ name }) => {
      if (name === "start") start(ctx, "focus");
      if (name === "stop") stop(ctx);
    });
    // Adanotch yeniden başladıysa süren turu kaldığı yerden sürdür.
    const state = load(ctx);
    if (state.endsAt && state.endsAt > Date.now()) schedule(ctx, state.endsAt);
  },

  update(ctx) {
    const { phase, endsAt, done } = load(ctx);
    const running = phase !== "idle" && endsAt !== null;
    return {
      compact: running
        ? { leading: ui.icon(phase === "focus" ? "brain.head.profile" : "cup.and.saucer"), trailing: ui.timer(endsAt) }
        : { leading: ui.icon("timer", { tone: "secondary" }) },
      expanded: ui.stack([
        running
          ? ui.timer(endsAt, { textStyle: "display", tone: phase === "focus" ? "accent" : "success" })
          : ui.text("Hazır", { style: "display", tone: "secondary" }),
        ui.text(`Bugün ${done} tur`, { style: "caption", tone: "secondary" }),
        ui.stack(
          [
            running
              ? ui.button("Durdur", "stop", { icon: "stop.fill" })
              : ui.button(`${ctx.settings.focusMinutes} dk odaklan`, "start", { icon: "play.fill", primary: true }),
          ],
          { axis: "h" },
        ),
      ]),
    };
  },

  onAction(action, ctx) {
    if (action === "start") start(ctx, "focus");
    if (action === "stop") stop(ctx);
  },
});

function load(ctx: Adanotch.Context<Settings>): State {
  return ctx.storage.get<State>("state") ?? { phase: "idle", endsAt: null, done: 0 };
}

function save(ctx: Adanotch.Context<Settings>, state: State) {
  ctx.storage.set("state", state);
}

function start(ctx: Adanotch.Context<Settings>, phase: Exclude<Phase, "idle">) {
  const minutes = phase === "focus" ? ctx.settings.focusMinutes : ctx.settings.breakMinutes;
  const endsAt = Date.now() + minutes * 60_000;
  save(ctx, { ...load(ctx), phase, endsAt });
  schedule(ctx, endsAt);
  ctx.refresh();
}

function stop(ctx: Adanotch.Context<Settings>) {
  clearTimeout(timer);
  save(ctx, { ...load(ctx), phase: "idle", endsAt: null });
  island.hide("pomodoro");
  ctx.refresh();
}

function schedule(ctx: Adanotch.Context<Settings>, endsAt: number) {
  clearTimeout(timer);
  timer = setTimeout(() => finish(ctx), endsAt - Date.now());
}

function finish(ctx: Adanotch.Context<Settings>) {
  const state = load(ctx);
  if (state.phase === "focus") {
    save(ctx, { ...state, done: state.done + 1 });
    notify.island({ text: "Mola zamanı", icon: "cup.and.saucer", tone: "success", duration: 5 });
    start(ctx, "break");
  } else {
    notify.island({ text: "Mola bitti", icon: "brain.head.profile", tone: "accent" });
    stop(ctx);
  }
}

GitHub PR'ları

Keychain'de saklanan bir token (secret ayarı), kimlik doğrulamalı fetch, tıklanabilir liste ve open().

const { activity, ui, secrets, open } = AdanotchKit;

interface Settings {
  query: string;
}

interface SearchResult {
  total_count: number;
  items: {
    number: number;
    title: string;
    html_url: string;
    draft?: boolean;
    repository_url: string;
    user: { login: string; avatar_url: string };
  }[];
}

activity<Settings>({
  async update(ctx) {
    const token = await secrets.get("token");
    if (!token) {
      return {
        compact: { leading: ui.icon("arrow.triangle.pull", { tone: "secondary" }) },
        expanded: ui.markdown("Ayarlar › Eklentiler'den bir **GitHub token** gir."),
      };
    }

    const url = new URL("https://api.github.com/search/issues");
    url.searchParams.set("q", `is:pr is:open ${ctx.settings.query}`);
    url.searchParams.set("per_page", "20");

    const res = await fetch(url, {
      headers: {
        authorization: `Bearer ${token}`,
        accept: "application/vnd.github+json",
        "x-github-api-version": "2022-11-28",
      },
    });
    if (res.status === 401) {
      return { expanded: ui.text("Token geçersiz", { tone: "danger" }) };
    }
    if (!res.ok) throw new Error(`GitHub ${res.status}`);
    const { total_count, items } = await res.json<SearchResult>();

    return {
      compact: {
        leading: ui.icon("arrow.triangle.pull"),
        trailing: ui.badge(String(total_count), { tone: total_count > 0 ? "accent" : "secondary" }),
      },
      expanded: ui.scroll(
        ui.list(
          items.map((pr) => ({
            title: pr.title,
            subtitle: `${pr.repository_url.split("/").slice(-2).join("/")} #${pr.number} · ${pr.user.login}`,
            icon: pr.draft ? "pencil.circle" : "arrow.triangle.pull",
            tone: pr.draft ? ("secondary" as const) : ("default" as const),
            action: "open",
            value: pr.html_url,
          })),
        ),
        { maxHeight: 280 },
      ),
    };
  },

  async onAction(action, _ctx, value) {
    if (action === "open" && typeof value === "string") await open(value);
  },
});

GitHub token için ince ayarlı bir token oluştur; yalnız okuma izni (Pull requests: Read) yeter.

Bu sayfada