mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

* Update to latest Node LTS * Add sveltekit * Split tslib into separate @generated and @tslib components SvelteKit's path aliases don't support multiple locations, so our old approach of using @tslib to refer to both ts/lib and out/ts/lib will no longer work. Instead, all generated sources and their includes are placed in a separate out/ts/generated folder, and imported via @generated instead. This also allows us to generate .ts files, instead of needing to output separate .d.ts and .js files. * Switch package.json to module type * Avoid usage of baseUrl Incompatible with SvelteKit * Move sass into ts; use relative links SvelteKit's default sass support doesn't allow overriding loadPaths * jest->vitest, graphs example working with yarn dev * most pages working in dev mode * Some fixes after rebasing * Fix/silence some svelte-check errors * Get image-occlusion working with Fabric types * Post-rebase lock changes * Editor is now checked * SvelteKit build integrated into ninja * Use the new SvelteKit entrypoint for pages like congrats/deck options/etc * Run eslint once for ts/**; fix some tests * Fix a bunch of issues introduced when rebasing over latest main * Run eslint fix * Fix remaining eslint+pylint issues; tests now all pass * Fix some issues with a clean build * Latest bufbuild no longer requires @__PURE__ hack * Add a few missed dependencies * Add yarn.bat to fix Windows build * Fix pages failing to show when ANKI_API_PORT not defined * Fix svelte-check and vitest on Windows * Set node path in ./yarn * Move svelte-kit output to ts/.svelte-kit Sadly, I couldn't figure out a way to store it in out/ if out/ is a symlink, as it breaks module resolution when SvelteKit is run. * Allow HMR inside Anki * Skip SvelteKit build when HMR is defined * Fix some post-rebase issues I should have done a normal merge instead.
206 lines
7.6 KiB
TypeScript
206 lines
7.6 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import type { DeckNameId, DeckNames } from "@generated/anki/decks_pb";
|
|
import type { CsvMetadata, CsvMetadata_Delimiter, ImportResponse } from "@generated/anki/import_export_pb";
|
|
import { type CsvMetadata_MappedNotetype } from "@generated/anki/import_export_pb";
|
|
import type { NotetypeNameId, NotetypeNames } from "@generated/anki/notetypes_pb";
|
|
import { getCsvMetadata, getFieldNames, importCsv } from "@generated/backend";
|
|
import * as tr from "@generated/ftl";
|
|
import { cloneDeep, isEqual, noop } from "lodash-es";
|
|
import type { Readable, Writable } from "svelte/store";
|
|
import { readable, writable } from "svelte/store";
|
|
|
|
export interface ColumnOption {
|
|
label: string;
|
|
shortLabel?: string;
|
|
value: number;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export function getGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype | null {
|
|
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
|
}
|
|
|
|
export function getDeckId(meta: CsvMetadata): bigint | null {
|
|
return meta.deck.case === "deckId" ? meta.deck.value : null;
|
|
}
|
|
|
|
export class ImportCsvState {
|
|
readonly path: string;
|
|
readonly deckNameIds: DeckNameId[];
|
|
readonly notetypeNameIds: NotetypeNameId[];
|
|
|
|
readonly defaultDelimiter: CsvMetadata_Delimiter;
|
|
readonly defaultIsHtml: boolean;
|
|
readonly defaultNotetypeId: bigint | null;
|
|
readonly defaultDeckId: bigint | null;
|
|
|
|
readonly metadata: Writable<CsvMetadata>;
|
|
readonly globalNotetype: Writable<CsvMetadata_MappedNotetype | null>;
|
|
readonly deckId: Writable<bigint | null>;
|
|
readonly fieldNames: Readable<Promise<string[]>>;
|
|
readonly columnOptions: Readable<ColumnOption[]>;
|
|
|
|
private lastMetadata: CsvMetadata;
|
|
private lastGlobalNotetype: CsvMetadata_MappedNotetype | null;
|
|
private lastDeckId: bigint | null;
|
|
private fieldNamesSetter: (val: Promise<string[]>) => void = noop;
|
|
private columnOptionsSetter: (val: ColumnOption[]) => void = noop;
|
|
|
|
constructor(path: string, notetypes: NotetypeNames, decks: DeckNames, metadata: CsvMetadata) {
|
|
this.path = path;
|
|
this.deckNameIds = decks.entries;
|
|
this.notetypeNameIds = notetypes.entries;
|
|
|
|
this.lastMetadata = cloneDeep(metadata);
|
|
this.metadata = writable(metadata);
|
|
this.metadata.subscribe(this.onMetadataChanged.bind(this));
|
|
|
|
const globalNotetype = getGlobalNotetype(metadata);
|
|
this.lastGlobalNotetype = cloneDeep(getGlobalNotetype(metadata));
|
|
this.globalNotetype = writable(cloneDeep(globalNotetype));
|
|
this.globalNotetype.subscribe(this.onGlobalNotetypeChanged.bind(this));
|
|
|
|
this.lastDeckId = getDeckId(metadata);
|
|
this.deckId = writable(getDeckId(metadata));
|
|
this.deckId.subscribe(this.onDeckIdChanged.bind(this));
|
|
|
|
this.fieldNames = readable(
|
|
globalNotetype === null
|
|
? Promise.resolve([])
|
|
: getFieldNames({ ntid: globalNotetype.id }).then((list) => list.vals),
|
|
(set) => {
|
|
this.fieldNamesSetter = set;
|
|
},
|
|
);
|
|
|
|
this.columnOptions = readable(getColumnOptions(metadata), (set) => {
|
|
this.columnOptionsSetter = set;
|
|
});
|
|
|
|
this.defaultDelimiter = metadata.delimiter;
|
|
this.defaultIsHtml = metadata.isHtml;
|
|
this.defaultNotetypeId = this.lastGlobalNotetype?.id || null;
|
|
this.defaultDeckId = this.lastDeckId;
|
|
}
|
|
|
|
doImport(): Promise<ImportResponse> {
|
|
return importCsv({
|
|
path: this.path,
|
|
metadata: { ...this.lastMetadata, preview: [] },
|
|
}, { alertOnError: false });
|
|
}
|
|
|
|
private async onMetadataChanged(changed: CsvMetadata) {
|
|
if (isEqual(changed, this.lastMetadata)) {
|
|
return;
|
|
}
|
|
|
|
const shouldRefetchMetadata = this.shouldRefetchMetadata(changed);
|
|
if (shouldRefetchMetadata) {
|
|
changed = await getCsvMetadata({
|
|
path: this.path,
|
|
delimiter: changed.delimiter,
|
|
notetypeId: getGlobalNotetype(changed)?.id,
|
|
deckId: getDeckId(changed) ?? undefined,
|
|
isHtml: changed.isHtml,
|
|
});
|
|
}
|
|
|
|
const globalNotetype = getGlobalNotetype(changed);
|
|
this.globalNotetype.set(globalNotetype);
|
|
if (globalNotetype !== null && globalNotetype.id !== getGlobalNotetype(this.lastMetadata)?.id) {
|
|
this.fieldNamesSetter(getFieldNames({ ntid: globalNotetype.id }).then((list) => list.vals));
|
|
}
|
|
if (this.shouldRebuildColumnOptions(changed)) {
|
|
this.columnOptionsSetter(getColumnOptions(changed));
|
|
}
|
|
|
|
this.lastMetadata = cloneDeep(changed);
|
|
if (shouldRefetchMetadata) {
|
|
this.metadata.set(changed);
|
|
}
|
|
}
|
|
|
|
private shouldRefetchMetadata(changed: CsvMetadata): boolean {
|
|
return changed.delimiter !== this.lastMetadata.delimiter || changed.isHtml !== this.lastMetadata.isHtml
|
|
|| getGlobalNotetype(changed)?.id !== getGlobalNotetype(this.lastMetadata)?.id;
|
|
}
|
|
|
|
private shouldRebuildColumnOptions(changed: CsvMetadata): boolean {
|
|
return !isEqual(changed.columnLabels, this.lastMetadata.columnLabels)
|
|
|| !isEqual(changed.preview[0], this.lastMetadata.preview[0]);
|
|
}
|
|
|
|
private onGlobalNotetypeChanged(globalNotetype: CsvMetadata_MappedNotetype | null) {
|
|
if (isEqual(globalNotetype, this.lastGlobalNotetype)) {
|
|
return;
|
|
}
|
|
this.lastGlobalNotetype = cloneDeep(globalNotetype);
|
|
if (globalNotetype !== null) {
|
|
this.metadata.update((metadata) => {
|
|
metadata.notetype.value = globalNotetype;
|
|
return metadata;
|
|
});
|
|
}
|
|
}
|
|
|
|
private onDeckIdChanged(deckId: bigint | null) {
|
|
if (deckId === this.lastDeckId) {
|
|
return;
|
|
}
|
|
this.lastDeckId = deckId;
|
|
if (deckId !== null) {
|
|
this.metadata.update((metadata) => {
|
|
metadata.deck.value = deckId;
|
|
return metadata;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function getColumnOptions(
|
|
metadata: CsvMetadata,
|
|
): ColumnOption[] {
|
|
const notetypeColumn = getNotetypeColumn(metadata);
|
|
const deckColumn = getDeckColumn(metadata);
|
|
return [{ label: tr.changeNotetypeNothing(), value: 0, disabled: false }].concat(
|
|
metadata.columnLabels.map((label, index) => {
|
|
index += 1;
|
|
if (index === notetypeColumn) {
|
|
return columnOption(tr.notetypesNotetype(), true, index);
|
|
} else if (index === deckColumn) {
|
|
return columnOption(tr.decksDeck(), true, index);
|
|
} else if (index === metadata.guidColumn) {
|
|
return columnOption("GUID", true, index);
|
|
} else if (label === "") {
|
|
return columnOption(metadata.preview[0].vals[index - 1], false, index, true);
|
|
} else {
|
|
return columnOption(label, false, index);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
function columnOption(
|
|
label: string,
|
|
disabled: boolean,
|
|
index: number,
|
|
shortLabel?: boolean,
|
|
): ColumnOption {
|
|
return {
|
|
label: label ? `${index}: ${label}` : index.toString(),
|
|
shortLabel: shortLabel ? index.toString() : undefined,
|
|
value: index,
|
|
disabled,
|
|
};
|
|
}
|
|
|
|
function getDeckColumn(meta: CsvMetadata): number | null {
|
|
return meta.deck.case === "deckColumn" ? meta.deck.value : null;
|
|
}
|
|
|
|
function getNotetypeColumn(meta: CsvMetadata): number | null {
|
|
return meta.notetype.case === "notetypeColumn" ? meta.notetype.value : null;
|
|
}
|