diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py
index 2499bdcc8..276fe067a 100644
--- a/qt/aqt/deckoptions.py
+++ b/qt/aqt/deckoptions.py
@@ -4,6 +4,7 @@
from __future__ import annotations
import aqt
+from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom
from aqt.webview import AnkiWebView
@@ -38,7 +39,11 @@ class DeckOptionsDialog(QDialog):
self.setLayout(layout)
deck_id = self.mw.col.decks.get_current_id()
- self.web.eval(f"anki.deckConfig(document.getElementById('main'), {deck_id});")
+ self.web.eval(
+ f"""const $deckOptions = anki.deckConfig(
+ document.getElementById('main'), {deck_id});"""
+ )
+ gui_hooks.deck_options_did_load(self)
def reject(self) -> None:
self.web = None
diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py
index 3d611a988..346ba9c25 100644
--- a/qt/tools/genhooks_gui.py
+++ b/qt/tools/genhooks_gui.py
@@ -218,8 +218,8 @@ hooks = [
content.stats += "\n
my html
"
""",
),
- # Deck options
- ###################
+ # Deck options (legacy screen)
+ ###############################
Hook(
name="deck_conf_did_setup_ui_form",
args=["deck_conf: aqt.deckconf.DeckConf"],
@@ -287,6 +287,15 @@ hooks = [
],
doc="Called before config group is renamed",
),
+ # Deck options (new screen)
+ ############################
+ Hook(
+ name="deck_options_did_load",
+ args=[
+ "deck_options: aqt.deckoptions.DeckOptionsDialog",
+ ],
+ doc="Can be used to inject extra options into the config screen",
+ ),
# Filtered deck options
###################
Hook(
diff --git a/ts/deckconfig/Addons.svelte b/ts/deckconfig/Addons.svelte
new file mode 100644
index 000000000..0cb69b9af
--- /dev/null
+++ b/ts/deckconfig/Addons.svelte
@@ -0,0 +1,21 @@
+
+
+
+{#if $components.length}
+
+
Add-ons
+
+ {#each $components as addon}
+
+ {/each}
+
+{/if}
diff --git a/ts/deckconfig/BUILD.bazel b/ts/deckconfig/BUILD.bazel
index 4c464a618..1f3c69437 100644
--- a/ts/deckconfig/BUILD.bazel
+++ b/ts/deckconfig/BUILD.bazel
@@ -65,6 +65,7 @@ ts_library(
"//ts:image_module_support",
"//ts/lib",
"//ts/lib:backend_proto",
+ "//ts/sveltelib",
"@npm//lodash-es",
"@npm//svelte",
],
diff --git a/ts/deckconfig/ConfigEditor.svelte b/ts/deckconfig/ConfigEditor.svelte
index c01f1420b..c540d27d4 100644
--- a/ts/deckconfig/ConfigEditor.svelte
+++ b/ts/deckconfig/ConfigEditor.svelte
@@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ReviewOptions from "./ReviewOptions.svelte";
import LapseOptions from "./LapseOptions.svelte";
import GeneralOptions from "./GeneralOptions.svelte";
+ import Addons from "./Addons.svelte";
import type { DeckConfigState } from "./lib";
export let state: DeckConfigState;
@@ -29,4 +30,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+
diff --git a/ts/deckconfig/DeckConfigPage.svelte b/ts/deckconfig/DeckConfigPage.svelte
index 596ef2624..3762f0cf1 100644
--- a/ts/deckconfig/DeckConfigPage.svelte
+++ b/ts/deckconfig/DeckConfigPage.svelte
@@ -8,8 +8,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { DeckConfigState } from "./lib";
import { onMount, onDestroy } from "svelte";
import { registerShortcut } from "lib/shortcuts";
+ import type { Writable } from "svelte/store";
+ import HtmlAddon from "./HtmlAddon.svelte";
export let state: DeckConfigState;
+ let addons = state.addonComponents;
+
+ export function auxData(): Writable> {
+ return state.currentAuxData;
+ }
+
+ export function addHtmlAddon(html: string, mounted: () => void): void {
+ $addons = [
+ ...$addons,
+ {
+ component: HtmlAddon,
+ html,
+ mounted,
+ },
+ ];
+ }
let registerCleanup: () => void;
onMount(() => {
diff --git a/ts/deckconfig/HtmlAddon.svelte b/ts/deckconfig/HtmlAddon.svelte
new file mode 100644
index 000000000..70a8578b7
--- /dev/null
+++ b/ts/deckconfig/HtmlAddon.svelte
@@ -0,0 +1,14 @@
+
+
+
+{@html html}
diff --git a/ts/deckconfig/index.ts b/ts/deckconfig/index.ts
index db4de5fae..58695377c 100644
--- a/ts/deckconfig/index.ts
+++ b/ts/deckconfig/index.ts
@@ -9,14 +9,14 @@ import DeckConfigPage from "./DeckConfigPage.svelte";
export async function deckConfig(
target: HTMLDivElement,
deckId: number
-): Promise {
+): Promise {
checkNightMode();
await setupI18n({
modules: [ModuleName.SCHEDULING, ModuleName.ACTIONS, ModuleName.DECK_CONFIG],
});
const info = await getDeckConfigInfo(deckId);
const state = new DeckConfigState(deckId, info);
- new DeckConfigPage({
+ return new DeckConfigPage({
target,
props: { state },
});
diff --git a/ts/deckconfig/lib.test.ts b/ts/deckconfig/lib.test.ts
index 39db17e22..be129a521 100644
--- a/ts/deckconfig/lib.test.ts
+++ b/ts/deckconfig/lib.test.ts
@@ -33,6 +33,8 @@ const exampleData = {
leechAction: "LEECH_ACTION_TAG_ONLY",
leechThreshold: 8,
capAnswerTimeToSecs: 60,
+ other:
+ "eyJuZXciOnsic2VwYXJhdGUiOnRydWV9LCJyZXYiOnsiZnV6eiI6MC4wNSwibWluU3BhY2UiOjF9fQ==",
},
},
useCount: 1,
@@ -276,3 +278,54 @@ test("saving", () => {
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
expect(out.configs.map((c) => c.name)).toStrictEqual(["Default"]);
});
+
+test("aux data", () => {
+ const state = startingState();
+ expect(get(state.currentAuxData)).toStrictEqual({});
+ state.currentAuxData.update((val) => {
+ return { ...val, hello: "world" };
+ });
+
+ // check default
+ state.setCurrentIndex(0);
+ expect(get(state.currentAuxData)).toStrictEqual({
+ new: {
+ separate: true,
+ },
+ rev: {
+ fuzz: 0.05,
+ minSpace: 1,
+ },
+ });
+ state.currentAuxData.update((val) => {
+ return { ...val, defaultAddition: true };
+ });
+
+ // ensure changes serialize
+ const out = state.dataForSaving(true);
+ expect(out.configs.length).toBe(2);
+ const json = out.configs.map(
+ (c) =>
+ JSON.parse(new TextDecoder().decode((c.config as any).other)) as Record<
+ string,
+ unknown
+ >
+ );
+ expect(json).toStrictEqual([
+ // other deck comes first
+ {
+ hello: "world",
+ },
+ // default is selected, so comes last
+ {
+ defaultAddition: true,
+ new: {
+ separate: true,
+ },
+ rev: {
+ fuzz: 0.05,
+ minSpace: 1,
+ },
+ },
+ ]);
+});
diff --git a/ts/deckconfig/lib.ts b/ts/deckconfig/lib.ts
index 7e69eb1f0..0a639ef3b 100644
--- a/ts/deckconfig/lib.ts
+++ b/ts/deckconfig/lib.ts
@@ -10,6 +10,7 @@ import { postRequest } from "lib/postrequest";
import { Writable, writable, get, Readable, readable } from "svelte/store";
import { isEqual, cloneDeep } from "lodash-es";
import * as tr from "lib/i18n";
+import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
export async function getDeckConfigInfo(
deckId: number
@@ -50,10 +51,12 @@ export interface ConfigListEntry {
type ConfigInner = pb.BackendProto.DeckConfig.Config;
export class DeckConfigState {
readonly currentConfig: Writable;
+ readonly currentAuxData: Writable>;
readonly configList: Readable;
readonly parentLimits: Readable;
readonly currentDeck: pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
readonly defaults: ConfigInner;
+ readonly addonComponents: Writable;
private targetDeckId: number;
private configs: ConfigWithCount[];
@@ -69,8 +72,9 @@ export class DeckConfigState {
this.currentDeck = data.currentDeck as pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
this.defaults = data.defaults!.config! as ConfigInner;
this.configs = data.allConfig.map((config) => {
+ const configInner = config.config as pb.BackendProto.DeckConfig;
return {
- config: config.config as pb.BackendProto.DeckConfig,
+ config: configInner,
useCount: config.useCount!,
};
});
@@ -83,6 +87,7 @@ export class DeckConfigState {
// selected one at display time
this.configs[this.selectedIdx].useCount -= 1;
this.currentConfig = writable(this.getCurrentConfig());
+ this.currentAuxData = writable(this.getCurrentAuxData());
this.configList = readable(this.getConfigList(), (set) => {
this.configListSetter = set;
return;
@@ -92,6 +97,7 @@ export class DeckConfigState {
return;
});
this.schemaModified = data.schemaModified;
+ this.addonComponents = writable([]);
// create a temporary subscription to force our setters to be set immediately,
// so unit tests don't get stale results
@@ -100,6 +106,7 @@ export class DeckConfigState {
// update our state when the current config is changed
this.currentConfig.subscribe((val) => this.onCurrentConfigChanged(val));
+ this.currentAuxData.subscribe((val) => this.onCurrentAuxDataChanged(val));
}
setCurrentIndex(index: number): void {
@@ -192,13 +199,27 @@ export class DeckConfigState {
}
private onCurrentConfigChanged(config: ConfigInner): void {
- if (!isEqual(config, this.configs[this.selectedIdx].config.config)) {
- this.configs[this.selectedIdx].config.config = config;
- this.configs[this.selectedIdx].config.mtimeSecs = 0;
+ const configOuter = this.configs[this.selectedIdx].config;
+ if (!isEqual(config, configOuter.config)) {
+ configOuter.config = config;
+ if (configOuter.id) {
+ this.modifiedConfigs.add(configOuter.id);
+ }
}
this.parentLimitsSetter?.(this.getParentLimits());
}
+ private onCurrentAuxDataChanged(data: Record): void {
+ const current = this.getCurrentAuxData();
+ if (!isEqual(current, data)) {
+ this.currentConfig.update((config) => {
+ const asBytes = new TextEncoder().encode(JSON.stringify(data));
+ config.other = asBytes;
+ return config;
+ });
+ }
+ }
+
private ensureNewNameUnique(name: string): string {
const idx = this.configs.findIndex((e) => e.config.name === name);
if (idx !== -1) {
@@ -210,6 +231,7 @@ export class DeckConfigState {
private updateCurrentConfig(): void {
this.currentConfig.set(this.getCurrentConfig());
+ this.currentAuxData.set(this.getCurrentAuxData());
this.parentLimitsSetter?.(this.getParentLimits());
}
@@ -222,6 +244,12 @@ export class DeckConfigState {
return cloneDeep(this.configs[this.selectedIdx].config.config as ConfigInner);
}
+ /// Extra data associated with current config (for add-ons)
+ private getCurrentAuxData(): Record {
+ const conf = this.configs[this.selectedIdx].config.config as ConfigInner;
+ return bytesToObject(conf.other);
+ }
+
private getConfigList(): ConfigListEntry[] {
const list: ConfigListEntry[] = this.configs.map((c, idx) => {
const useCount = c.useCount + (idx === this.selectedIdx ? 1 : 0);
@@ -258,3 +286,24 @@ export class DeckConfigState {
};
}
}
+
+function bytesToObject(bytes: Uint8Array): Record {
+ if (!bytes.length) {
+ return {} as Record;
+ }
+
+ let obj: Record;
+ try {
+ obj = JSON.parse(new TextDecoder().decode(bytes)) as Record;
+ } catch (err) {
+ console.log(`invalid json in deck config`);
+ return {} as Record;
+ }
+
+ if (obj.constructor !== Object) {
+ console.log(`invalid object in deck config`);
+ return {} as Record;
+ }
+
+ return obj;
+}