Skip to content

Commit

Permalink
feat(all): embed options (#60)
Browse files Browse the repository at this point in the history
* feat(all): embed options

* docs: Create embed-options.md
  • Loading branch information
ikkz authored Jan 11, 2025
1 parent f796da5 commit 5b5e34a
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-penguins-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'anki-templates': minor
---

feat(all): embed options (嵌入选项)
11 changes: 11 additions & 0 deletions build/rollup.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ export async function rollupOptions(config) {
fileName: `front.html`,
template({ files }) {
let frontHtml = '';
frontHtml += `<script>
window.atDefaultOptions =
/* options begin */
{
"at:test:test": "test"
}
/* options end */
</script>
`;
frontHtml += `<div data-at-version="${packageJson.version}" id="at-root"></div>`;
frontHtml += `<style>${files?.css?.map(({ source }) => source).join('')}</style>`;
frontHtml += `
Expand Down
12 changes: 12 additions & 0 deletions docs/embed-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Embedding Options in Templates

Since Anki does not provide the ability to store data in templates, there are some issues with user preference settings in templates:
- Cannot sync across multiple devices
- Preferences may be lost when restarting Anki on some clients

To resolve these issues, you need to paste the formatted template settings below into the template code. This process involves two steps:
1. Open the card template settings in Anki. The method to access this varies by platform, so please refer to the official user documentation.
2. Find the "Front Template" of this template and paste the formatted template configuration you see on the settings page into the corresponding location. Below is an example image from the Mac version of Anki, other versions may display differently:
<img width="1033" alt="图片" src="https://github.com/user-attachments/assets/e59139c1-2b22-422f-8eaa-d7bb528aa472" />

These modifications may be overwritten when upgrading the template, so please backup the formatted template settings before upgrading.
1 change: 1 addition & 0 deletions src/hooks/use-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum Page {
Index = 'index',
Settings = 'settings',
Tools = 'tools',
Options = 'options',
}

export type PageMap = Partial<Record<Page, FC>>;
Expand Down
2 changes: 2 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import OptionsPage from './options';
import SettingsPage from './settings';
import ToolsPage from './tools';
import { Page, PageMap } from '@/hooks/use-page';

export const DEFAULT_PAGES: PageMap = {
[Page.Settings]: SettingsPage,
[Page.Tools]: ToolsPage,
[Page.Options]: OptionsPage,
};
38 changes: 38 additions & 0 deletions src/pages/options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Block } from '@/components/block';
import { useNavigate, Page } from '@/hooks/use-page';
import * as t from 'at/i18n';
import { id } from 'at/options';
import { useMemo } from 'react';

export default () => {
const navigate = useNavigate();
const options = useMemo(() => {
const options = Object.fromEntries(
Object.keys(localStorage)
.filter(
(key) =>
(key.startsWith('at:_global:') || key.startsWith(`at:${id}:`)) &&
!!localStorage.getItem(key),
)
.map((key) => [key, localStorage.getItem(key)]),
);
return JSON.stringify(options, undefined, 2);
}, []);

return (
<Block
name={t.optionsPage}
action={t.back}
onAction={() => navigate(Page.Settings)}
>
<div className="prose prose-sm prose-neutral dark:prose-invert">
<p
dangerouslySetInnerHTML={{
__html: t.optionsHelp,
}}
/>
<pre>{options}</pre>
</div>
</Block>
);
};
4 changes: 4 additions & 0 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export default () => {
action={t.back}
onAction={() => navigate(Page.Index)}
>
<div className="text-gray-500 dark:text-gray-400 text-sm mb-3">
{t.optionsHint}
<Button onClick={() => navigate(Page.Options)}>{t.optionsPage}</Button>
</div>
<div className="flex flex-col gap-4">
<OptionList />
</div>
Expand Down
65 changes: 49 additions & 16 deletions src/utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ import { isAnkiDroid } from './bridge';
import { getAnkiStorage } from 'anki-storage';
import { id } from 'at/options';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { doNothing } from 'remeda';

declare global {
interface Window {
atDefaultOptions?: Record<string, string>;
}
}

const { atDefaultOptions } = window;

// set default options to localStorage when this template type is displayed first time
// users can still change the options, which will take effects during this Anki execution
const initKey = `at:init:${id}`;
if (atDefaultOptions && !sessionStorage.getItem(initKey)) {
Object.entries(atDefaultOptions).forEach(([key, value]) => {
localStorage.setItem(key, value);
});
sessionStorage.setItem(initKey, 'true');
}
const defaultOptionKeys = Object.keys(atDefaultOptions || {});

function isDefaultOptionKey(key: string) {
return defaultOptionKeys.includes(key);
}

function createAnkiDroidStorage() {
const as: ReturnType<typeof getAnkiStorage> = new Promise((resolve) => {
Expand All @@ -13,33 +37,42 @@ function createAnkiDroidStorage() {

return createJSONStorage<any>(() => ({
getItem(key) {
as.then((api) => {
api.localStorage.getItem(key).then((value) => {
if (value !== localStorage.getItem(key)) {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
if (!isDefaultOptionKey(key)) {
as.then((api) => {
api.localStorage.getItem(key).then((value) => {
if (value !== localStorage.getItem(key)) {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
}
updaters.get(key)?.forEach((fn) => fn(value));
}
updaters.get(key)?.forEach((fn) => fn(value));
}
});
});
});
}
return localStorage.getItem(key);
},
setItem(key, newValue) {
localStorage.setItem(key, newValue);
as.then((api) => {
api.localStorage.setItem(key, newValue);
});
if (!isDefaultOptionKey(key)) {
as.then((api) => {
api.localStorage.setItem(key, newValue);
});
}
},
removeItem(key) {
localStorage.removeItem(key);
as.then((api) => {
api.localStorage.removeItem(key);
});
if (!isDefaultOptionKey(key)) {
as.then((api) => {
api.localStorage.removeItem(key);
});
}
},
subscribe(key, callback) {
if (isDefaultOptionKey(key)) {
return doNothing;
}
if (updaters.has(key)) {
updaters.get(key)?.push(callback);
} else {
Expand Down
7 changes: 5 additions & 2 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"question": "Question",
"singleAnswer": "One answer",
"multipleAnswer": "Multiple answers",
"templateSetting": "Template settings",
"templateSetting": "Settings",
"back": "Back",
"missingAnswer": "Missing answer",
"missingOptions": "Missing options",
Expand Down Expand Up @@ -49,5 +49,8 @@
"translate": "Translate",
"search": "Search",
"explainFollowing": "Explain the following content: ",
"toolHelp": "<p>Each tool requires a URL to be set. For example, Google Search:</p><pre><code>https://www.google.com/search?q={q}</code></pre><p>When you select text and click the corresponding tool, the <code>{q}</code> in the link will be replaced with the selected text, and the browser will automatically navigate to the updated link.</p><p>Prefix text and suffix text refer to adding text before and after the selected text, which is then used as <code>{q}</code> for replacement.</p>"
"toolHelp": "<p>Each tool requires a URL to be set. For example, Google Search:</p><pre><code>https://www.google.com/search?q={q}</code></pre><p>When you select text and click the corresponding tool, the <code>{q}</code> in the link will be replaced with the selected text, and the browser will automatically navigate to the updated link.</p><p>Prefix text and suffix text refer to adding text before and after the selected text, which is then used as <code>{q}</code> for replacement.</p>",
"optionsHint": "If the settings are lost after reopening Anki, or if you want the following settings to sync across devices, please go to ",
"optionsPage": "Options Configuration",
"optionsHelp": "The following content can be selected and copied. For detailed steps, please refer to <a target=\"_blank\" href=\"https://github.com/ikkz/anki-template/blob/main/docs/embed-options.md\">here</a>"
}
7 changes: 5 additions & 2 deletions translations/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"question": "题目",
"singleAnswer": "单选",
"multipleAnswer": "多选",
"templateSetting": "模板设置",
"templateSetting": "设置",
"back": "返回",
"missingAnswer": "缺少答案",
"missingOptions": "缺少选项",
Expand Down Expand Up @@ -49,5 +49,8 @@
"translate": "翻译",
"search": "搜索",
"explainFollowing": "解释以下内容: ",
"toolHelp": "<p>每一个工具都需要设置 url。以谷歌搜索为例</p><pre><code>https://www.google.com/search?q={q}</code></pre><p>在选中文本后点击对应的工具时,链接中的 <code>{q}</code> 将会被替换为您选择的文本,然后将自动跳转到替换后的链接。</p><p>前置文本和后置文本是指在选中的文本前后添加对应的文本,然后作为 <code>{q}</code> 来执行替换。</p>"
"toolHelp": "<p>每一个工具都需要设置 url。以谷歌搜索为例</p><pre><code>https://www.google.com/search?q={q}</code></pre><p>在选中文本后点击对应的工具时,链接中的 <code>{q}</code> 将会被替换为您选择的文本,然后将自动跳转到替换后的链接。</p><p>前置文本和后置文本是指在选中的文本前后添加对应的文本,然后作为 <code>{q}</code> 来执行替换。</p>",
"optionsHint": "如果在重新打开Anki之后丢失了以前的设置,或者希望以下设置在设备之间同步,请前往",
"optionsPage": "选项配置",
"optionsHelp": "以下内容可以选中复制,详细步骤请查看 <a target=\"_blank\" href=\"https://github.com/ikkz/anki-template/blob/main/docs/embed-options.md\">这里</a>"
}

0 comments on commit 5b5e34a

Please sign in to comment.