basic support for add-ons in new deck config screen
- expose the data as a writable store - currently only supports raw HTML; example to come - fix changes not marking a deck config as modified - the data is currently packed into the deckconfig object, but we may move these to a separate store in the collection config in the future, like is done with decks/notetypes
This commit is contained in:
parent
c71b684a94
commit
e2a4d6041c
@ -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
|
||||
|
@ -218,8 +218,8 @@ hooks = [
|
||||
content.stats += "\n<div>my html</div>"
|
||||
""",
|
||||
),
|
||||
# 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(
|
||||
|
21
ts/deckconfig/Addons.svelte
Normal file
21
ts/deckconfig/Addons.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
// import * as tr from "lib/i18n";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let state: DeckConfigState;
|
||||
let components = state.addonComponents;
|
||||
</script>
|
||||
|
||||
{#if $components.length}
|
||||
<div>
|
||||
<h2>Add-ons</h2>
|
||||
|
||||
{#each $components as addon}
|
||||
<svelte:component this={addon.component} {state} {...addon} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
@ -65,6 +65,7 @@ ts_library(
|
||||
"//ts:image_module_support",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"//ts/sveltelib",
|
||||
"@npm//lodash-es",
|
||||
"@npm//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
|
||||
<ReviewOptions {state} />
|
||||
<LapseOptions {state} />
|
||||
<GeneralOptions {state} />
|
||||
<Addons {state} />
|
||||
</div>
|
||||
|
@ -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<Record<string, unknown>> {
|
||||
return state.currentAuxData;
|
||||
}
|
||||
|
||||
export function addHtmlAddon(html: string, mounted: () => void): void {
|
||||
$addons = [
|
||||
...$addons,
|
||||
{
|
||||
component: HtmlAddon,
|
||||
html,
|
||||
mounted,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let registerCleanup: () => void;
|
||||
onMount(() => {
|
||||
|
14
ts/deckconfig/HtmlAddon.svelte
Normal file
14
ts/deckconfig/HtmlAddon.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let html: string;
|
||||
export let mounted: () => void;
|
||||
|
||||
onMount(mounted);
|
||||
</script>
|
||||
|
||||
{@html html}
|
@ -9,14 +9,14 @@ import DeckConfigPage from "./DeckConfigPage.svelte";
|
||||
export async function deckConfig(
|
||||
target: HTMLDivElement,
|
||||
deckId: number
|
||||
): Promise<void> {
|
||||
): Promise<DeckConfigPage> {
|
||||
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 },
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -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<ConfigInner>;
|
||||
readonly currentAuxData: Writable<Record<string, unknown>>;
|
||||
readonly configList: Readable<ConfigListEntry[]>;
|
||||
readonly parentLimits: Readable<ParentLimits>;
|
||||
readonly currentDeck: pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
|
||||
readonly defaults: ConfigInner;
|
||||
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
|
||||
|
||||
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<string, unknown>): 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<string, unknown> {
|
||||
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<string, unknown> {
|
||||
if (!bytes.length) {
|
||||
return {} as Record<string, unknown>;
|
||||
}
|
||||
|
||||
let obj: Record<string, unknown>;
|
||||
try {
|
||||
obj = JSON.parse(new TextDecoder().decode(bytes)) as Record<string, unknown>;
|
||||
} catch (err) {
|
||||
console.log(`invalid json in deck config`);
|
||||
return {} as Record<string, unknown>;
|
||||
}
|
||||
|
||||
if (obj.constructor !== Object) {
|
||||
console.log(`invalid object in deck config`);
|
||||
return {} as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user