Plugin
A Kiku plugin is a JavaScript module named _kiku_plugin.js. This module must export a named variable called plugin. The type definitions for this module are available here.
ts
import type {
createComputed,
createContext,
createEffect,
createMemo,
createResource,
createSignal,
ErrorBoundary,
For,
getOwner,
lazy,
Match,
onCleanup,
onMount,
runWithOwner,
Show,
Suspense,
Switch,
untrack,
useContext,
} from "solid-js";
import type h from "solid-js/h";
import type { JSX } from "solid-js/jsx-runtime";
import type { createStore } from "solid-js/store";
import type { Portal } from "solid-js/web";
import type { PitchInfo } from "#/components/_kiku_lazy/util/hatsuon";
import type { UseAnkiFieldContext } from "#/components/shared/AnkiFieldsContext";
import type { UseBreakpointContext } from "#/components/shared/BreakpointContext";
import type { UseCardContext } from "#/components/shared/CardContext";
import type { UseConfigContext } from "#/components/shared/ConfigContext";
import type { AnkiBackFields, AnkiDroidAPI, AnkiFrontFields } from "#/types";
export type Ctx = {
h: typeof h;
createSignal: typeof createSignal;
createEffect: typeof createEffect;
createMemo: typeof createMemo;
createResource: typeof createResource;
createComputed: typeof createComputed;
onMount: typeof onMount;
onCleanup: typeof onCleanup;
createContext: typeof createContext;
useContext: typeof useContext;
lazy: typeof lazy;
ErrorBoundary: typeof ErrorBoundary;
For: typeof For;
Portal: typeof Portal;
Show: typeof Show;
Suspense: typeof Suspense;
Switch: typeof Switch;
Match: typeof Match;
untrack: typeof untrack;
runWithOwner: typeof runWithOwner;
getOwner: typeof getOwner;
createStore: typeof createStore;
//
ankiFields: AnkiFrontFields | AnkiBackFields;
ankiDroidAPI: () => AnkiDroidAPI | undefined;
useAnkiFieldContext: UseAnkiFieldContext;
useBreakpointContext: UseBreakpointContext;
useCardContext: UseCardContext;
useConfigContext: UseConfigContext;
};
export type KikuPlugin = {
ExternalLinks?: (props: {
DefaultExternalLinks: () => JSX.Element;
ctx: Ctx;
}) => JSX.Element | JSX.Element[];
Sentence?: (props: {
DefaultSentence: () => JSX.Element;
ctx: Ctx;
}) => JSX.Element | JSX.Element[];
Pitch?: (props: {
pitchInfo: PitchInfo;
index: number;
DefaultPitch: (props: {
pitchInfo: PitchInfo;
index: number;
ref?: (ref: HTMLDivElement) => void;
}) => JSX.Element;
ctx: Ctx;
}) => JSX.Element | JSX.Element[];
onPluginLoad?: (props: { ctx: Ctx }) => void;
onSettingsMount?: (props: { ctx: Ctx }) => void;
};The plugin system is currently very basic, but more APIs will be added in the future.
ExternalLinks
If defined, this component will be mounted under the Definition section on the back side of the note, replacing the default ExternalLinks.
Example
js
/**
* @import { KikuPlugin } from "#/plugins/pluginTypes";
*/
/**
* @type { KikuPlugin }
*/
export const plugin = {
ExternalLinks: (props) => {
const h = props.ctx.h;
// Create an arbitrary link
const NadeshikoLink = h(
"a",
{
href: (() => {
const url = new URL("https://nadeshiko.co/search/sentence");
url.searchParams.set("query", props.ctx.ankiFields.Expression);
return url.toString();
})(),
target: "_blank",
},
h("img", {
class: "size-5 object-contain rounded-xs",
src: "https://nadeshiko.co/favicon.ico",
}),
);
// Create an arbitrary button with a custom on:click handler
const AnkiDroidBrowseButton = h(
"button",
{
class: "text-sm btn btn-sm",
"on:click": () => {
props.ctx
.ankiDroidAPI()
?.ankiSearchCard(
`("note:Kiku" OR "note:Lapis") AND "Expression:*${props.ctx.ankiFields.Expression}*"`,
);
},
},
"Browse",
);
return [
// includes the default ExternalLinks
props.DefaultExternalLinks(),
NadeshikoLink(),
AnkiDroidBrowseButton(),
// you can create as many links as you want
];
},
};Sentence
If defined, this component will replace the default Sentence component.
Example
js
/**
* @import { KikuPlugin } from "#/plugins/pluginTypes";
*/
/**
* @type { KikuPlugin }
*/
export const plugin = {
Sentence: (props) => {
const h = props.ctx.h;
function SentenceTranslation() {
const translation =
"SentenceTranslation" in props.ctx.ankiFields
? props.ctx.ankiFields?.SentenceTranslation
: document.getElementById("SentenceTranslation")?.innerHTML;
if (!translation) return null;
return h("div", {
class: "text-lg text-base-content-calm sentence-translation",
innerHTML: translation,
})();
}
// You can inline the CSS here
const style = h(
"style",
`
.sentence-translation { filter: blur(4px); }
.sentence-translation:hover { filter: none; }
`,
);
return [props.DefaultSentence(), SentenceTranslation(), style()];
},
};Pitch
If defined, this component will replace the default Pitch component.
Example
js
/**
* @import { KikuPlugin } from "#/plugins/pluginTypes";
*/
/**
* @type { KikuPlugin }
*/
export const plugin = {
Pitch: (props) => {
const h = props.ctx.h;
const onMount = props.ctx.onMount;
const createSignal = props.ctx.createSignal;
const DefaultPitch = props.DefaultPitch;
const pitchInfo = props.pitchInfo;
const color = () => {
// you can customize the color here
switch (pitchInfo.pitchNum) {
case 0:
return { color: "#d46a6a", colorContent: "#8b3f3f" };
case 1:
return { color: "#6ad46a", colorContent: "#3f8b3f" };
case 2:
return { color: "#6a6ad4", colorContent: "#3f3f8b" };
case 3:
return { color: "#d4d46a", colorContent: "#8b8b3f" };
default:
return { color: "#8b8b8b", colorContent: "#3f3f3f" };
}
};
const css = `
.custom-pitch {
color: ${color().color};
border-color: ${color().color}
}
.custom-pitch::after {
background-color: ${color().color};
}
`;
const style = h("style", css);
const [ref, setRef] = createSignal();
onMount(() => {
const el = ref();
if (!el) return;
const spans = Array.from(el.querySelectorAll("[data-is-even] span"));
spans.forEach((span) => {
span.classList.add("custom-pitch");
});
const indicator = el.querySelector("[data-is-even]")?.nextElementSibling;
indicator.style.backgroundColor = color()?.color;
indicator.style.color = color()?.colorContent;
});
return [
DefaultPitch({
index: props.index,
pitchInfo: props.pitchInfo,
ref: setRef,
}),
style(),
];
},
};onPluginLoad
This function is called when the plugin is loaded.
Example
js
/**
* @import { KikuPlugin } from "#/plugins/pluginTypes";
*/
/**
* @type { KikuPlugin }
*/
export const plugin = {
onPluginLoad: () => {
const root = KIKU_STATE.root;
const settingsMounted = sessionStorage.getItem("settings-mounted");
// stop if settings has ever been mounted
if (settingsMounted) return;
// unblur NSFW automatically if it's not work time
if (!isWorkTime() && root) {
root.dataset.blurNsfw = "false";
}
},
};
function isWorkTime(workdays = [1, 2, 3, 4, 5], startHour = 9, endHour = 17) {
const now = new Date();
const day = now.getDay(); // 0 = Sun, 6 = Sat
const hour = now.getHours();
const mins = now.getMinutes();
const isWeekday = workdays.includes(day);
// Compare hour + minutes together
const nowMinutes = hour * 60 + mins;
const startMinutes = startHour * 60;
const endMinutes = endHour * 60;
const isWorkHours = nowMinutes >= startMinutes && nowMinutes < endMinutes;
return isWeekday && isWorkHours;
}onSettingsMount
This function is called when the settings page is mounted.
Example
js
/**
* @import { KikuPlugin } from "#/plugins/pluginTypes";
*/
/**
* @type { KikuPlugin }
*/
export const plugin = {
onSettingsMount: () => {
sessionStorage.setItem("settings-mounted", "true");
},
};