anki/ts/change-notetype/lib.ts
Damien Elmes f3b6deefe9 Combine all backend methods into a single js/d.ts file, like in Python
Easier to import from, and allows us to declare the output of the build
action without having to iterate over all the proto filenames. Have
confirmed it doesn't break esbuild's tree shaking.
2023-07-03 13:46:38 +10:00

199 lines
6.5 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { ChangeNotetypeInfo, ChangeNotetypeRequest, NotetypeNames } from "@tslib/anki/notetypes_pb";
import { changeNotetype, getChangeNotetypeInfo } from "@tslib/backend";
import * as tr from "@tslib/ftl";
import { isEqual } from "lodash-es";
import type { Readable } from "svelte/store";
import { readable } from "svelte/store";
function nullToNegativeOne(list: (number | null)[]): number[] {
return list.map((val) => val ?? -1);
}
/** Public only for tests. */
export function negativeOneToNull(list: number[]): (number | null)[] {
return list.map((val) => (val === -1 ? null : val));
}
/** Wrapper for the protobuf message to make it more ergonomic. */
export class ChangeNotetypeInfoWrapper {
fields: (number | null)[];
templates?: (number | null)[];
oldNotetypeName: string;
readonly info: ChangeNotetypeInfo;
constructor(info: ChangeNotetypeInfo) {
this.info = info;
const templates = info.input?.newTemplates ?? [];
if (templates.length > 0) {
this.templates = negativeOneToNull(templates);
}
this.fields = negativeOneToNull(info.input?.newFields ?? []);
this.oldNotetypeName = info.oldNotetypeName;
}
/** A list with an entry for each field/template in the new notetype, with
the values pointing back to indexes in the original notetype. */
mapForContext(ctx: MapContext): (number | null)[] {
return ctx == MapContext.Template ? this.templates ?? [] : this.fields;
}
/** Return index of old fields/templates, with null values mapped to "Nothing"
at the end.*/
getOldIndex(ctx: MapContext, newIdx: number): number {
const map = this.mapForContext(ctx);
const val = map[newIdx];
return val ?? this.getOldNamesIncludingNothing(ctx).length - 1;
}
/** Return all the old names, with "Nothing" at the end. */
getOldNamesIncludingNothing(ctx: MapContext): string[] {
return [...this.getOldNames(ctx), tr.changeNotetypeNothing()];
}
/** Old names without "Nothing" at the end. */
getOldNames(ctx: MapContext): string[] {
return ctx == MapContext.Template
? this.info.oldTemplateNames
: this.info.oldFieldNames;
}
getOldNotetypeName(): string {
return this.info.oldNotetypeName;
}
getNewName(ctx: MapContext, idx: number): string {
return (
ctx == MapContext.Template
? this.info.newTemplateNames
: this.info.newFieldNames
)[idx];
}
unusedItems(ctx: MapContext): string[] {
const usedEntries = new Set(this.mapForContext(ctx).filter((v) => v !== null));
const oldNames = this.getOldNames(ctx);
const unusedIdxs = [...Array(oldNames.length).keys()].filter(
(idx) => !usedEntries.has(idx),
);
const unusedNames = unusedIdxs.map((idx) => oldNames[idx]);
return unusedNames;
}
unchanged(): boolean {
return (
this.input().newNotetypeId === this.input().oldNotetypeId
&& isEqual(this.fields, [...Array(this.fields.length).keys()])
&& isEqual(this.templates, [...Array(this.templates?.length ?? 0).keys()])
);
}
input(): ChangeNotetypeRequest {
return this.info.input as ChangeNotetypeRequest;
}
/** Pack changes back into input message for saving. */
intoInput(): ChangeNotetypeRequest {
const input = this.info.input as ChangeNotetypeRequest;
input.newFields = nullToNegativeOne(this.fields);
if (this.templates) {
input.newTemplates = nullToNegativeOne(this.templates);
}
return input;
}
}
export interface NotetypeListEntry {
idx: number;
name: string;
current: boolean;
}
export enum MapContext {
Field,
Template,
}
export class ChangeNotetypeState {
readonly info: Readable<ChangeNotetypeInfoWrapper>;
readonly notetypes: Readable<NotetypeListEntry[]>;
private info_: ChangeNotetypeInfoWrapper;
private infoSetter!: (val: ChangeNotetypeInfoWrapper) => void;
private notetypeNames: NotetypeNames;
private notetypesSetter!: (val: NotetypeListEntry[]) => void;
constructor(
notetypes: NotetypeNames,
info: ChangeNotetypeInfo,
) {
this.info_ = new ChangeNotetypeInfoWrapper(info);
this.info = readable(this.info_, (set) => {
this.infoSetter = set;
});
this.notetypeNames = notetypes;
this.notetypes = readable(this.buildNotetypeList(), (set) => {
this.notetypesSetter = set;
return;
});
}
async setTargetNotetypeIndex(idx: number): Promise<void> {
this.info_.input().newNotetypeId = this.notetypeNames.entries[idx].id!;
this.notetypesSetter(this.buildNotetypeList());
const { oldNotetypeId, newNotetypeId } = this.info_.input();
const newInfo = await getChangeNotetypeInfo({
oldNotetypeId,
newNotetypeId,
});
this.info_ = new ChangeNotetypeInfoWrapper(newInfo);
this.info_.unusedItems(MapContext.Field);
this.infoSetter(this.info_);
}
setOldIndex(ctx: MapContext, newIdx: number, oldIdx: number): void {
const list = this.info_.mapForContext(ctx);
const oldNames = this.info_.getOldNames(ctx);
const realOldIdx = oldIdx < oldNames.length ? oldIdx : null;
const allowDupes = ctx == MapContext.Field;
// remove any existing references?
if (!allowDupes && realOldIdx !== null) {
for (let i = 0; i < list.length; i++) {
if (list[i] === realOldIdx) {
list[i] = null;
}
}
}
list[newIdx] = realOldIdx;
this.infoSetter(this.info_);
}
dataForSaving(): ChangeNotetypeRequest {
return this.info_.intoInput();
}
async save(): Promise<void> {
if (this.info_.unchanged()) {
alert("No changes to save");
return;
}
await changeNotetype(this.dataForSaving());
}
private buildNotetypeList(): NotetypeListEntry[] {
const currentId = this.info_.input().newNotetypeId;
return this.notetypeNames.entries.map(
(entry, idx) => ({
idx,
name: entry.name,
current: entry.id === currentId,
} as NotetypeListEntry),
);
}
}