c79f8ba88f
The saved characters weren't worth the increased difficulty when reading, and the fact that we were deviating from protobuf norms.
222 lines
7.1 KiB
TypeScript
222 lines
7.1 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
/* eslint
|
|
@typescript-eslint/no-non-null-assertion: "off",
|
|
*/
|
|
|
|
import pb from "lib/backend_proto";
|
|
import { postRequest } from "lib/postrequest";
|
|
import { readable, Readable } from "svelte/store";
|
|
import { isEqual } from "lodash-es";
|
|
|
|
export async function getNotetypeNames(): Promise<pb.BackendProto.NotetypeNames> {
|
|
return pb.BackendProto.NotetypeNames.decode(
|
|
await postRequest("/_anki/notetypeNames", "")
|
|
);
|
|
}
|
|
|
|
export async function getChangeNotetypeInfo(
|
|
oldNotetypeId: number,
|
|
newNotetypeId: number
|
|
): Promise<pb.BackendProto.ChangeNotetypeInfo> {
|
|
return pb.BackendProto.ChangeNotetypeInfo.decode(
|
|
await postRequest(
|
|
"/_anki/changeNotetypeInfo",
|
|
JSON.stringify({ oldNotetypeId, newNotetypeId })
|
|
)
|
|
);
|
|
}
|
|
|
|
export async function changeNotetype(
|
|
input: pb.BackendProto.ChangeNotetypeRequest
|
|
): Promise<void> {
|
|
const data: Uint8Array =
|
|
pb.BackendProto.ChangeNotetypeRequest.encode(input).finish();
|
|
await postRequest("/_anki/changeNotetype", data);
|
|
return;
|
|
}
|
|
|
|
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)[];
|
|
readonly info: pb.BackendProto.ChangeNotetypeInfo;
|
|
|
|
constructor(info: pb.BackendProto.ChangeNotetypeInfo) {
|
|
this.info = info;
|
|
const templates = info.input!.newTemplates!;
|
|
if (templates.length > 0) {
|
|
this.templates = negativeOneToNull(templates);
|
|
}
|
|
this.fields = negativeOneToNull(info.input!.newFields!);
|
|
}
|
|
|
|
/// 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), "(Nothing)"];
|
|
}
|
|
|
|
/// Old names without "Nothing" at the end.
|
|
getOldNames(ctx: MapContext): string[] {
|
|
return ctx == MapContext.Template
|
|
? this.info.oldTemplateNames
|
|
: this.info.oldFieldNames;
|
|
}
|
|
|
|
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]);
|
|
unusedNames.sort();
|
|
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(): pb.BackendProto.ChangeNotetypeRequest {
|
|
return this.info.input as pb.BackendProto.ChangeNotetypeRequest;
|
|
}
|
|
|
|
/// Pack changes back into input message for saving.
|
|
intoInput(): pb.BackendProto.ChangeNotetypeRequest {
|
|
const input = this.info.input as pb.BackendProto.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: pb.BackendProto.NotetypeNames;
|
|
private notetypesSetter!: (val: NotetypeListEntry[]) => void;
|
|
|
|
constructor(
|
|
notetypes: pb.BackendProto.NotetypeNames,
|
|
info: pb.BackendProto.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 newInfo = await getChangeNotetypeInfo(
|
|
this.info_.input().oldNotetypeId,
|
|
this.info_.input().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 realOldIdx = oldIdx < list.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_);
|
|
}
|
|
|
|
async save(): Promise<void> {
|
|
if (this.info_.unchanged()) {
|
|
alert("No changes to save");
|
|
return;
|
|
}
|
|
await changeNotetype(this.dataForSaving());
|
|
}
|
|
|
|
dataForSaving(): pb.BackendProto.ChangeNotetypeRequest {
|
|
return this.info_.intoInput();
|
|
}
|
|
|
|
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)
|
|
);
|
|
}
|
|
}
|