mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
implement deck config saving on JS end
This commit is contained in:
parent
094c272294
commit
55277aa90a
8 changed files with 131 additions and 11 deletions
|
@ -34,7 +34,7 @@ LABEL_REQUIRED = 2
|
|||
LABEL_REPEATED = 3
|
||||
|
||||
# messages we don't want to unroll in codegen
|
||||
SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences"}
|
||||
SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences", "UpdateDeckConfigs"}
|
||||
SKIP_UNROLL_OUTPUT = {"GetPreferences"}
|
||||
|
||||
SKIP_DECODE = {"Graphs", "GetGraphPreferences"}
|
||||
|
|
|
@ -23,6 +23,7 @@ DeckNameId = _pb.DeckNameId
|
|||
FilteredDeckConfig = _pb.Deck.Filtered
|
||||
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
|
||||
DeckConfigsForUpdate = _pb.DeckConfigsForUpdate
|
||||
UpdateDeckConfigs = _pb.UpdateDeckConfigsIn
|
||||
|
||||
# legacy code may pass this in as the type argument to .id()
|
||||
defaultDeck = 0
|
||||
|
@ -328,6 +329,9 @@ class DeckManager:
|
|||
def get_deck_configs_for_update(self, deck_id: DeckId) -> DeckConfigsForUpdate:
|
||||
return self.col._backend.get_deck_configs_for_update(deck_id)
|
||||
|
||||
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
|
||||
return self.col._backend.update_deck_configs(input=input)
|
||||
|
||||
def all_config(self) -> List[DeckConfigDict]:
|
||||
"A list of all deck config."
|
||||
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
|
||||
|
|
|
@ -20,8 +20,10 @@ from waitress.server import create_server
|
|||
|
||||
import aqt
|
||||
from anki import hooks
|
||||
from anki.collection import GraphPreferences
|
||||
from anki.collection import GraphPreferences, OpChanges
|
||||
from anki.decks import UpdateDeckConfigs
|
||||
from anki.utils import devMode, from_json_bytes
|
||||
from aqt.operations.deck import update_deck_configs
|
||||
from aqt.qt import *
|
||||
from aqt.utils import aqt_data_folder
|
||||
|
||||
|
@ -283,11 +285,32 @@ def deck_configs_for_update() -> bytes:
|
|||
).SerializeToString()
|
||||
|
||||
|
||||
def update_deck_configs_request() -> bytes:
|
||||
# the regular change tracking machinery expects to be started on the main
|
||||
# thread and uses a callback on success, so we need to run this op on
|
||||
# main, and return immediately from the web request
|
||||
|
||||
input = UpdateDeckConfigs()
|
||||
input.ParseFromString(request.data)
|
||||
|
||||
def on_success(changes: OpChanges) -> None:
|
||||
print("done", changes)
|
||||
|
||||
def handle_on_main() -> None:
|
||||
update_deck_configs(parent=aqt.mw, input=input).success(
|
||||
on_success
|
||||
).run_in_background()
|
||||
|
||||
aqt.mw.taskman.run_on_main(handle_on_main)
|
||||
return b""
|
||||
|
||||
|
||||
post_handlers = {
|
||||
"graphData": graph_data,
|
||||
"graphPreferences": graph_preferences,
|
||||
"setGraphPreferences": set_graph_preferences,
|
||||
"deckConfigsForUpdate": deck_configs_for_update,
|
||||
"updateDeckConfigs": update_deck_configs_request,
|
||||
# pylint: disable=unnecessary-lambda
|
||||
"i18nResources": i18n_resources,
|
||||
"congratsInfo": congrats_info,
|
||||
|
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
from typing import Optional, Sequence
|
||||
|
||||
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
|
||||
from anki.decks import DeckCollapseScope, DeckId
|
||||
from anki.decks import DeckCollapseScope, DeckId, UpdateDeckConfigs
|
||||
from aqt import QWidget
|
||||
from aqt.operations import CollectionOp
|
||||
from aqt.utils import getOnlyText, tooltip, tr
|
||||
|
@ -80,3 +80,9 @@ def set_deck_collapsed(
|
|||
|
||||
def set_current_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.decks.set_current(deck_id))
|
||||
|
||||
|
||||
def update_deck_configs(
|
||||
*, parent: QWidget, input: UpdateDeckConfigs
|
||||
) -> CollectionOp[OpChanges]:
|
||||
return CollectionOp(parent, lambda col: col.decks.update_deck_configs(input))
|
||||
|
|
|
@ -54,6 +54,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function save(applyToChildDecks: boolean): void {
|
||||
state.save(applyToChildDecks);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -63,7 +67,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</style>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary">Save</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
on:click={() => save(false)}>Save</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||
|
@ -82,6 +89,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<li><a class="dropdown-item" href={'#'}>Apply to Child Decks</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href={'#'} on:click={() => save(true)}>Save to All
|
||||
Children</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function deckConfig(
|
|||
modules: [ModuleName.SCHEDULING, ModuleName.ACTIONS, ModuleName.DECK_CONFIG],
|
||||
});
|
||||
const info = await getDeckConfigInfo(deckId);
|
||||
const state = new DeckConfigState(info);
|
||||
const state = new DeckConfigState(deckId, info);
|
||||
new DeckConfigPage({
|
||||
target,
|
||||
props: { state },
|
||||
|
|
|
@ -92,6 +92,7 @@ const exampleData = {
|
|||
|
||||
function startingState(): DeckConfigState {
|
||||
return new DeckConfigState(
|
||||
123,
|
||||
pb.BackendProto.DeckConfigsForUpdate.fromObject(exampleData)
|
||||
);
|
||||
}
|
||||
|
@ -198,7 +199,8 @@ test("deck list", () => {
|
|||
expect(get(state.currentConfig).newPerDay).toBe(10);
|
||||
|
||||
// only the pre-existing deck should be listed for removal
|
||||
expect((state as any).removedConfigs).toStrictEqual([1618570764780]);
|
||||
const out = state.dataForSaving(false);
|
||||
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
|
||||
});
|
||||
|
||||
test("duplicate name", () => {
|
||||
|
@ -233,3 +235,44 @@ test("parent counts", () => {
|
|||
});
|
||||
expect(get(state.parentLimits)).toStrictEqual({ newCards: 123, reviews: 200 });
|
||||
});
|
||||
|
||||
test("saving", () => {
|
||||
let state = startingState();
|
||||
let out = state.dataForSaving(false);
|
||||
expect(out.removedConfigIds).toStrictEqual([]);
|
||||
expect(out.targetDeckId).toBe(123);
|
||||
// in no-changes case, currently selected config should
|
||||
// be returned
|
||||
expect(out.configs.length).toBe(1);
|
||||
expect(out.configs[0].name).toBe("another one");
|
||||
expect(out.applyToChildren).toBe(false);
|
||||
|
||||
// rename, then change current deck
|
||||
state.setCurrentName("zzz");
|
||||
out = state.dataForSaving(true);
|
||||
state.setCurrentIndex(0);
|
||||
|
||||
// renamed deck should be in changes, with current deck as last element
|
||||
out = state.dataForSaving(true);
|
||||
expect(out.configs.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
|
||||
expect(out.applyToChildren).toBe(true);
|
||||
|
||||
// start again, adding new deck
|
||||
state = startingState();
|
||||
state.addConfig("hello");
|
||||
|
||||
// deleting it should not change removedConfigs
|
||||
state.removeCurrentConfig();
|
||||
out = state.dataForSaving(true);
|
||||
expect(out.removedConfigIds).toStrictEqual([]);
|
||||
|
||||
// select the other non-default deck & remove
|
||||
state.setCurrentIndex(1);
|
||||
state.removeCurrentConfig();
|
||||
|
||||
// should be listed in removedConfigs, and modified should
|
||||
// only contain Default, which is the new current deck
|
||||
out = state.dataForSaving(true);
|
||||
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
|
||||
expect(out.configs.map((c) => c.name)).toStrictEqual(["Default"]);
|
||||
});
|
||||
|
|
|
@ -19,6 +19,14 @@ export async function getDeckConfigInfo(
|
|||
);
|
||||
}
|
||||
|
||||
export async function saveDeckConfig(
|
||||
input: pb.BackendProto.UpdateDeckConfigsIn
|
||||
): Promise<void> {
|
||||
const data: Uint8Array = pb.BackendProto.UpdateDeckConfigsIn.encode(input).finish();
|
||||
await postRequest("/_anki/updateDeckConfigs", data);
|
||||
return;
|
||||
}
|
||||
|
||||
export type DeckConfigId = number;
|
||||
|
||||
export interface ConfigWithCount {
|
||||
|
@ -47,6 +55,7 @@ export class DeckConfigState {
|
|||
readonly currentDeck: pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
|
||||
readonly defaults: ConfigInner;
|
||||
|
||||
private targetDeckId: number;
|
||||
private configs: ConfigWithCount[];
|
||||
private selectedIdx: number;
|
||||
private configListSetter!: (val: ConfigListEntry[]) => void;
|
||||
|
@ -55,7 +64,8 @@ export class DeckConfigState {
|
|||
private removedConfigs: DeckConfigId[] = [];
|
||||
private schemaModified: boolean;
|
||||
|
||||
constructor(data: pb.BackendProto.DeckConfigsForUpdate) {
|
||||
constructor(targetDeckId: number, data: pb.BackendProto.DeckConfigsForUpdate) {
|
||||
this.targetDeckId = targetDeckId;
|
||||
this.currentDeck = data.currentDeck as pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
|
||||
this.defaults = data.defaults!.config! as ConfigInner;
|
||||
this.configs = data.allConfig.map((config) => {
|
||||
|
@ -117,7 +127,6 @@ export class DeckConfigState {
|
|||
}
|
||||
|
||||
/// Adds a new config, making it current.
|
||||
/// not already a new config.
|
||||
addConfig(name: string): void {
|
||||
const uniqueName = this.ensureNewNameUnique(name);
|
||||
const config = pb.BackendProto.DeckConfig.create({
|
||||
|
@ -144,9 +153,8 @@ export class DeckConfigState {
|
|||
removeCurrentConfig(): void {
|
||||
const currentId = this.configs[this.selectedIdx].config.id;
|
||||
if (currentId === 1) {
|
||||
throw "can't remove default config";
|
||||
throw Error("can't remove default config");
|
||||
}
|
||||
|
||||
if (currentId !== 0) {
|
||||
this.removedConfigs.push(currentId);
|
||||
this.schemaModified = true;
|
||||
|
@ -157,6 +165,32 @@ export class DeckConfigState {
|
|||
this.updateConfigList();
|
||||
}
|
||||
|
||||
dataForSaving(applyToChildren: boolean): pb.BackendProto.UpdateDeckConfigsIn {
|
||||
const modifiedConfigsExcludingCurrent = this.configs
|
||||
.map((c) => c.config)
|
||||
.filter((c, idx) => {
|
||||
return (
|
||||
idx !== this.selectedIdx &&
|
||||
(c.id === 0 || this.modifiedConfigs.has(c.id))
|
||||
);
|
||||
});
|
||||
const configs = [
|
||||
...modifiedConfigsExcludingCurrent,
|
||||
// current must come last, even if unmodified
|
||||
this.configs[this.selectedIdx].config,
|
||||
];
|
||||
return pb.BackendProto.UpdateDeckConfigsIn.create({
|
||||
targetDeckId: this.targetDeckId,
|
||||
removedConfigIds: this.removedConfigs,
|
||||
configs,
|
||||
applyToChildren,
|
||||
});
|
||||
}
|
||||
|
||||
async save(applyToChildren: boolean): Promise<void> {
|
||||
await saveDeckConfig(this.dataForSaving(applyToChildren));
|
||||
}
|
||||
|
||||
private onCurrentConfigChanged(config: ConfigInner): void {
|
||||
if (!isEqual(config, this.configs[this.selectedIdx].config.config)) {
|
||||
this.configs[this.selectedIdx].config.config = config;
|
||||
|
|
Loading…
Reference in a new issue