// 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; isCloze: boolean; readonly info: ChangeNotetypeInfo; constructor(info: ChangeNotetypeInfo) { this.info = info; const templates = info.input?.newTemplates ?? []; this.isCloze = info.input?.isCloze ?? false; 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; readonly notetypes: Readable; 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 { 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 { 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), ); } }