Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tof): Add new True/False template #15

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/serious-panthers-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'anki-templates': minor
---

feat(tf): Add new True/False template (新增正误题模板)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"ahooks": "^3.8.1",
"clsx": "^2.1.1",
"jotai": "^2.9.3",
"lucide-react": "^0.468.0",
"preact": "^10.23.2",
"react-error-boundary": "^4.1.1",
"react-use": "^17.5.1",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions release.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,21 @@
}
}
]
},
"tof": {
"id": {
"zh": 1356462000,
"en": 1693362483
},
"deck": 1598429921,
"notes": [
{
"fields": {
"question": "This is the stem of the question. It supports various content formats in Anki, including bold, formulas, etc.",
"items": "<ul><li>T: All sub-questions should be in an unordered list format</li><li>T: Each sub-question must begin with \"T:\" or \"F:\", indicating whether the sub-question is true or false<br></li><li>T: Pay special attention to ensuring \"T/F\" is followed by an English half-width colon<br></li></ul>",
"note": "note"
}
}
]
}
}
112 changes: 43 additions & 69 deletions src/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,53 @@ export const blurOptionsAtom = atomWithLocalStorage<boolean>(
false,
);

const CommonOptions: FC = () => {
const [selectionMenu, setSelectionMenu] = useAtom(selectionMenuAtom);
const [hideAbout, setHideAbout] = useAtom(hideAboutAtom);
const [biggerText, setBiggerText] = useAtom(biggerTextAtom);
const [hideTimer, setHideTimer] = useAtom(hideTimerAtom);
const [noScorll, setNoScorll] = useAtom(noScorllAtom);

return (
<>
<Checkbox
title={t('biggerText')}
checked={biggerText}
onChange={setBiggerText}
/>
<Checkbox
title={t('noScroll')}
checked={noScorll}
onChange={setNoScorll}
/>
<Checkbox
title={t('selMenu')}
subtitle={t('selMenuDetail')}
checked={selectionMenu}
onChange={setSelectionMenu}
/>
<Checkbox
title={t('hideTimer')}
checked={hideTimer}
onChange={setHideTimer}
/>
<Checkbox
title={t('hideAbout')}
checked={hideAbout}
onChange={setHideAbout}
/>
</>
);
};

let OptionList: FC;

// these branches can be treeshaken by rollup
if (id === 'mcq') {
OptionList = () => {
const [randomOptions, setRandomOptions] = useAtom(randomOptionsAtom);
const [selectionMenu, setSelectionMenu] = useAtom(selectionMenuAtom);
const [hideAbout, setHideAbout] = useAtom(hideAboutAtom);
const [biggerText, setBiggerText] = useAtom(biggerTextAtom);
const [hideTimer, setHideTimer] = useAtom(hideTimerAtom);
const [hideQuestionType, setHideQuestionType] =
useAtom(hideQuestionTypeAtom);
const [noScorll, setNoScorll] = useAtom(noScorllAtom);
const [blurOptions, setBlurOptions] = useAtom(blurOptionsAtom);

return (
Expand All @@ -53,86 +87,26 @@ if (id === 'mcq') {
onChange={setHideQuestionType}
subtitle={t('hideQuestionTypeDetail')}
/>
<Checkbox
title={t('biggerText')}
checked={biggerText}
onChange={setBiggerText}
/>
<Checkbox
title={t('randomOption')}
subtitle={t('randomOptionDetail')}
checked={randomOptions}
onChange={setRandomOptions}
/>
<Checkbox
title={t('noScroll')}
checked={noScorll}
onChange={setNoScorll}
/>
<Checkbox
title={t('selMenu')}
subtitle={t('selMenuDetail')}
checked={selectionMenu}
onChange={setSelectionMenu}
/>
<Checkbox
title={t('blurOptions')}
subtitle={t('blurOptionsDetail')}
checked={blurOptions}
onChange={setBlurOptions}
/>
<Checkbox
title={t('hideTimer')}
checked={hideTimer}
onChange={setHideTimer}
/>
<Checkbox
title={t('hideAbout')}
checked={hideAbout}
onChange={setHideAbout}
/>
<CommonOptions />
</>
);
};
} else if (id === 'basic') {
OptionList = () => {
const [selectionMenu, setSelectionMenu] = useAtom(selectionMenuAtom);
const [hideAbout, setHideAbout] = useAtom(hideAboutAtom);
const [biggerText, setBiggerText] = useAtom(biggerTextAtom);
const [hideTimer, setHideTimer] = useAtom(hideTimerAtom);
const [noScorll, setNoScorll] = useAtom(noScorllAtom);

return (
<>
<Checkbox
title={t('biggerText')}
checked={biggerText}
onChange={setBiggerText}
/>
<Checkbox
title={t('noScroll')}
checked={noScorll}
onChange={setNoScorll}
/>
<Checkbox
title={t('selMenu')}
subtitle={t('selMenuDetail')}
checked={selectionMenu}
onChange={setSelectionMenu}
/>
<Checkbox
title={t('hideTimer')}
checked={hideTimer}
onChange={setHideTimer}
/>
<Checkbox
title={t('hideAbout')}
checked={hideAbout}
onChange={setHideAbout}
/>
</>
);
};
OptionList = CommonOptions;
} else if (id === 'tof') {
OptionList = CommonOptions;
} else {
OptionList = () => null;
}
Expand Down
129 changes: 129 additions & 0 deletions src/entries/tof.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { CardShell } from '@/components/card-shell';
import { AnkiField } from '@/components/field';
import { useBack } from '@/hooks/use-back';
import { useCrossState } from '@/hooks/use-cross-state';
import { FIELD_ID } from '@/utils/const';
import { isFieldEmpty } from '@/utils/field';
import { t } from '@/utils/locale';
import useCreation from 'ahooks/es/useCreation';
import clsx from 'clsx';
import { CheckCircle, XCircle } from 'lucide-react';
import { useCallback } from 'react';

interface ItemProp {
index: number;
node: HTMLDivElement;
answer?: boolean;
}

const Item = ({ node, answer, index }: ItemProp) => {

Check failure on line 19 in src/entries/tof.tsx

View workflow job for this annotation

GitHub Actions / lint

'answer' is defined but never used

Check failure on line 19 in src/entries/tof.tsx

View workflow job for this annotation

GitHub Actions / lint

'answer' is defined but never used
const attachNode = useCallback(
(ref: HTMLDivElement) => {
if (node && ref) {
node.remove();
ref.appendChild(node);
}
},
[node],
);

const back = useBack();

Check failure on line 30 in src/entries/tof.tsx

View workflow job for this annotation

GitHub Actions / lint

'back' is assigned a value but never used

Check failure on line 30 in src/entries/tof.tsx

View workflow job for this annotation

GitHub Actions / lint

'back' is assigned a value but never used
const [status, setStatus] = useCrossState<boolean | undefined>(
`status-${index}`,
undefined,
);

return (
<div className="rounded-xl bg-indigo-50 px-4 py-2 mt-4 flex items-center justify-between">
<div
ref={attachNode}
className={clsx(
'prose prose-neutral dark:prose-invert',
'flex-grow mr-2',
)}
/>
<div className="flex space-x-2">
<div
className={clsx(
'p-2 rounded-full cursor-pointer',
'transition-transform hover:scale-105 active:scale-95',
status === true
? 'bg-green-500 text-white'
: 'bg-indigo-100 text-gray-600',
)}
onClick={() => setStatus(true)}
>
<CheckCircle size={24} />
</div>
<div
className={clsx(
'p-2 rounded-full cursor-pointer',
'transition-transform hover:scale-105 active:scale-95',
status === false
? 'bg-red-500 text-white'
: 'bg-indigo-100 text-gray-600',
)}
onClick={() => setStatus(false)}
>
<XCircle size={24} />
</div>
</div>
</div>
);
};

export default () => {
const items = useCreation(() => {
const field = document.getElementById(FIELD_ID('items'));
if (!field) {
return null;
}
const itemNodes = field
.querySelector('ul')
?.querySelectorAll(':scope > li');
if (!itemNodes?.length) {
return null;
}
return Array.from(itemNodes).map((node, idx) => {
const answer = !node.textContent?.startsWith('F:');
const container = document.createElement('div');
container.append(...Array.from(node.childNodes));

let firstTextNode: Node | null = container;
while (firstTextNode && firstTextNode.nodeType !== Node.TEXT_NODE) {
firstTextNode = firstTextNode.firstChild;
}
do {
if (!firstTextNode) {
break;
}
const match = firstTextNode.textContent?.match(/^(T|F):\s*/);
if (!match) {
break;
}
const range = document.createRange();
range.setStart(firstTextNode, 0);
range.setEnd(firstTextNode, match[0].length);
range.deleteContents();
// eslint-disable-next-line no-constant-condition
} while (false);
return <Item index={idx} key={idx} node={container} answer={answer} />;
});
}, []);
const hasNote = !isFieldEmpty(FIELD_ID('note'));

return (
<CardShell
title={t('question')}
questionExtra={items}
answer={
hasNote ? (
<AnkiField
name="note"
className={clsx('prose prose-sm mt-3', 'dark:prose-invert')}
/>
) : null
}
/>
);
};
5 changes: 5 additions & 0 deletions templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@
"id": "basic",
"name": "Basic",
"fields": ["question", "answer", "note", "Tags"]
},
"tof": {
"id": "tof",
"name": "True or False",
"fields": ["question", "items", "note", "Tags"]
}
}
Loading