mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Tooltips for CSV import and import page refactoring (#2655)
* Make enum selector generic * Refactor ImportCsvPage to support tooltips * Improve csv import defaults * Unify import pages * Improve import page styling * Fix life cycle issue with import properties * Remove size constraints to fix scrollbar styling * Add help strings and urls to csv import page * Show ErrorPage on ImportPage error * Fix escaping of import path * Unify ImportPage and ImportLogPage * Apply suggestions from code review (dae) * Fix import progress * Fix preview overflowing container * Don't include <br> in FileIoErrors (dae) e.g. 500: Failed to read '/home/dae/foo2.csv':<br>stream did not contain valid UTF-8 I thought about using {@html ...} here, but that's a potential security issue, as the filename is not something we control.
This commit is contained in:
parent
23823d3135
commit
850043b49b
52 changed files with 1055 additions and 941 deletions
|
@ -351,7 +351,7 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
|
|||
],
|
||||
)?;
|
||||
build_page(
|
||||
"import-log",
|
||||
"import-page",
|
||||
true,
|
||||
inputs![
|
||||
//
|
||||
|
|
|
@ -188,6 +188,36 @@ importing-note-updated-as-file-had-newer = Note updated, as file had newer versi
|
|||
importing-note-skipped-due-to-missing-notetype = Note skipped, as its notetype was missing
|
||||
importing-note-skipped-due-to-missing-deck = Note skipped, as its deck was missing
|
||||
importing-note-skipped-due-to-empty-first-field = Note skipped, as its first field is empty
|
||||
importing-field-separator-help =
|
||||
The character separating fields in the text file. You can use the preview to check
|
||||
if the fields are separated correctly.
|
||||
|
||||
Please note that if this character appears in any field itself, the field has to be
|
||||
quoted accordingly to the CSV standard. Spreadsheet programs like LibreOffice will
|
||||
do this automatically.
|
||||
importing-allow-html-in-fields-help =
|
||||
Enable this if the file contains HTML formatting. E.g. if the file contains the string
|
||||
'<br>', it will appear as a line break on your card. On the other hand, with this
|
||||
option disabled, the literal characters '<br>' will be rendered.
|
||||
importing-notetype-help =
|
||||
Newly-imported notes will have this notetype, and only existing notes with this
|
||||
notetype will be updated.
|
||||
|
||||
You can choose which fields in the file correspond to which notetype fields with the
|
||||
mapping tool.
|
||||
importing-deck-help = Imported cards will be placed in this deck.
|
||||
importing-existing-notes-help =
|
||||
What to do if an imported note matches an existing one.
|
||||
|
||||
- `{ importing-update }`: Update the existing note.
|
||||
- `{ importing-preserve }`: Do nothing.
|
||||
- `{ importing-duplicate }`: Create a new note.
|
||||
importing-match-scope-help =
|
||||
Only existing notes with the same notetype will be checked for duplicates. This can
|
||||
additionally be restricted to notes with cards in the same deck.
|
||||
importing-tag-all-notes-help =
|
||||
These tags will be added to both newly-imported and updated notes.
|
||||
importing-tag-updated-notes-help = These tags will be added to any updated notes.
|
||||
|
||||
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
import aqt
|
||||
import aqt.deckconf
|
||||
|
@ -14,45 +15,76 @@ from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGe
|
|||
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportArgs:
|
||||
path: str
|
||||
title = "importLog"
|
||||
kind = AnkiWebViewKind.IMPORT_LOG
|
||||
ts_page = "import-page"
|
||||
setup_function_name = "setupImportPage"
|
||||
|
||||
def args_json(self) -> str:
|
||||
return json.dumps(self.path)
|
||||
|
||||
|
||||
class JsonFileArgs(ImportArgs):
|
||||
def args_json(self) -> str:
|
||||
return json.dumps(dict(type="json_file", path=self.path))
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonStringArgs(ImportArgs):
|
||||
json: str
|
||||
|
||||
def args_json(self) -> str:
|
||||
return json.dumps(dict(type="json_string", path=self.path, json=self.json))
|
||||
|
||||
|
||||
class CsvArgs(ImportArgs):
|
||||
title = "csv import"
|
||||
kind = AnkiWebViewKind.IMPORT_CSV
|
||||
ts_page = "import-csv"
|
||||
setup_function_name = "setupImportCsvPage"
|
||||
|
||||
|
||||
class AnkiPackageArgs(ImportArgs):
|
||||
title = "anki package import"
|
||||
kind = AnkiWebViewKind.IMPORT_ANKI_PACKAGE
|
||||
ts_page = "import-anki-package"
|
||||
setup_function_name = "setupImportAnkiPackagePage"
|
||||
|
||||
|
||||
class ImportDialog(QDialog):
|
||||
TITLE: str
|
||||
KIND: AnkiWebViewKind
|
||||
TS_PAGE: str
|
||||
SETUP_FUNCTION_NAME: str
|
||||
DEFAULT_SIZE = (800, 800)
|
||||
MIN_SIZE = (400, 300)
|
||||
silentlyClose = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mw: aqt.main.AnkiQt,
|
||||
path: str,
|
||||
) -> None:
|
||||
def __init__(self, mw: aqt.main.AnkiQt, args: ImportArgs) -> None:
|
||||
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
||||
self.mw = mw
|
||||
self._setup_ui(path)
|
||||
self.args = args
|
||||
self._setup_ui()
|
||||
self.show()
|
||||
|
||||
def _setup_ui(self, path: str) -> None:
|
||||
def _setup_ui(self) -> None:
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.mw.garbage_collect_on_dialog_finish(self)
|
||||
self.setMinimumSize(*self.MIN_SIZE)
|
||||
disable_help_button(self)
|
||||
restoreGeom(self, self.TITLE, default_size=self.DEFAULT_SIZE)
|
||||
restoreGeom(self, self.args.title, default_size=self.DEFAULT_SIZE)
|
||||
addCloseShortcut(self)
|
||||
|
||||
self.web = AnkiWebView(kind=self.KIND)
|
||||
self.web = AnkiWebView(kind=self.args.kind)
|
||||
self.web.setVisible(False)
|
||||
self.web.load_ts_page(self.TS_PAGE)
|
||||
self.web.load_ts_page(self.args.ts_page)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.web)
|
||||
self.setLayout(layout)
|
||||
restoreGeom(self, self.TITLE, default_size=(800, 800))
|
||||
restoreGeom(self, self.args.title, default_size=(800, 800))
|
||||
|
||||
escaped_path = json.dumps(path.replace("'", r"\'"))
|
||||
self.web.evalWithCallback(
|
||||
f"anki.{self.SETUP_FUNCTION_NAME}({escaped_path});",
|
||||
f"anki.{self.args.setup_function_name}({self.args.args_json()});",
|
||||
lambda _: self.web.setFocus(),
|
||||
)
|
||||
self.setWindowTitle(tr.decks_import_file())
|
||||
|
@ -62,19 +94,5 @@ class ImportDialog(QDialog):
|
|||
self.mw.col.set_wants_abort()
|
||||
self.web.cleanup()
|
||||
self.web = None
|
||||
saveGeom(self, self.TITLE)
|
||||
saveGeom(self, self.args.title)
|
||||
QDialog.reject(self)
|
||||
|
||||
|
||||
class ImportCsvDialog(ImportDialog):
|
||||
TITLE = "csv import"
|
||||
KIND = AnkiWebViewKind.IMPORT_CSV
|
||||
TS_PAGE = "import-csv"
|
||||
SETUP_FUNCTION_NAME = "setupImportCsvPage"
|
||||
|
||||
|
||||
class ImportAnkiPackageDialog(ImportDialog):
|
||||
TITLE = "anki package import"
|
||||
KIND = AnkiWebViewKind.IMPORT_ANKI_PACKAGE
|
||||
TS_PAGE = "import-anki-package"
|
||||
SETUP_FUNCTION_NAME = "setupImportAnkiPackagePage"
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
import aqt
|
||||
import aqt.deckconf
|
||||
import aqt.main
|
||||
import aqt.operations
|
||||
from aqt.qt import *
|
||||
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
|
||||
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
||||
|
||||
|
||||
@dataclass
|
||||
class _CommonArgs:
|
||||
type: str = dataclasses.field(init=False)
|
||||
path: str
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(dataclasses.asdict(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonFileArgs(_CommonArgs):
|
||||
type = "json_file"
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonStringArgs(_CommonArgs):
|
||||
type = "json_string"
|
||||
json: str
|
||||
|
||||
|
||||
class ImportLogDialog(QDialog):
|
||||
GEOMETRY_KEY = "importLog"
|
||||
silentlyClose = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mw: aqt.main.AnkiQt,
|
||||
args: JsonFileArgs | JsonStringArgs,
|
||||
) -> None:
|
||||
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
||||
self.mw = mw
|
||||
self._setup_ui(args)
|
||||
self.show()
|
||||
|
||||
def _setup_ui(
|
||||
self,
|
||||
args: JsonFileArgs | JsonStringArgs,
|
||||
) -> None:
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.mw.garbage_collect_on_dialog_finish(self)
|
||||
self.setMinimumSize(400, 300)
|
||||
disable_help_button(self)
|
||||
addCloseShortcut(self)
|
||||
|
||||
self.web = AnkiWebView(kind=AnkiWebViewKind.IMPORT_LOG)
|
||||
self.web.setVisible(False)
|
||||
self.web.load_ts_page("import-log")
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.web)
|
||||
self.setLayout(layout)
|
||||
restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800))
|
||||
|
||||
self.web.evalWithCallback(
|
||||
"anki.setupImportLogPage(%s);" % (args.to_json()),
|
||||
lambda _: self.web.setFocus(),
|
||||
)
|
||||
|
||||
title = tr.importing_import_log()
|
||||
title += f" - {os.path.basename(args.path)}"
|
||||
self.setWindowTitle(title)
|
||||
|
||||
def reject(self) -> None:
|
||||
if self.mw.col and self.windowModality() == Qt.WindowModality.ApplicationModal:
|
||||
self.mw.col.set_wants_abort()
|
||||
self.web.cleanup()
|
||||
self.web = None
|
||||
saveGeom(self, self.GEOMETRY_KEY)
|
||||
QDialog.reject(self)
|
|
@ -13,9 +13,10 @@ from anki.collection import Collection, Progress
|
|||
from anki.errors import Interrupted
|
||||
from anki.foreign_data import mnemosyne
|
||||
from anki.lang import without_unicode_isolation
|
||||
from aqt.import_export.import_dialog import ImportAnkiPackageDialog, ImportCsvDialog
|
||||
from aqt.import_export.import_log_dialog import (
|
||||
ImportLogDialog,
|
||||
from aqt.import_export.import_dialog import (
|
||||
AnkiPackageArgs,
|
||||
CsvArgs,
|
||||
ImportDialog,
|
||||
JsonFileArgs,
|
||||
JsonStringArgs,
|
||||
)
|
||||
|
@ -87,7 +88,7 @@ class ApkgImporter(Importer):
|
|||
|
||||
@staticmethod
|
||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||
ImportAnkiPackageDialog(mw, path)
|
||||
ImportDialog(mw, AnkiPackageArgs(path))
|
||||
|
||||
|
||||
class MnemosyneImporter(Importer):
|
||||
|
@ -98,9 +99,7 @@ class MnemosyneImporter(Importer):
|
|||
QueryOp(
|
||||
parent=mw,
|
||||
op=lambda col: mnemosyne.serialize(path, col.decks.current()["id"]),
|
||||
success=lambda json: ImportLogDialog(
|
||||
mw, JsonStringArgs(path=path, json=json)
|
||||
),
|
||||
success=lambda json: ImportDialog(mw, JsonStringArgs(path=path, json=json)),
|
||||
).with_progress().run_in_background()
|
||||
|
||||
|
||||
|
@ -109,7 +108,7 @@ class CsvImporter(Importer):
|
|||
|
||||
@staticmethod
|
||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||
ImportCsvDialog(mw, path)
|
||||
ImportDialog(mw, CsvArgs(path))
|
||||
|
||||
|
||||
class JsonImporter(Importer):
|
||||
|
@ -117,7 +116,7 @@ class JsonImporter(Importer):
|
|||
|
||||
@staticmethod
|
||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||
ImportLogDialog(mw, JsonFileArgs(path=path))
|
||||
ImportDialog(mw, JsonFileArgs(path=path))
|
||||
|
||||
|
||||
IMPORTERS: list[Type[Importer]] = [
|
||||
|
|
|
@ -425,9 +425,8 @@ def import_done() -> bytes:
|
|||
def update_window_modality() -> None:
|
||||
if window := aqt.mw.app.activeWindow():
|
||||
from aqt.import_export.import_dialog import ImportDialog
|
||||
from aqt.import_export.import_log_dialog import ImportLogDialog
|
||||
|
||||
if isinstance(window, (ImportDialog, ImportLogDialog)):
|
||||
if isinstance(window, ImportDialog):
|
||||
window.hide()
|
||||
window.setWindowModality(Qt.WindowModality.NonModal)
|
||||
window.show()
|
||||
|
|
|
@ -48,7 +48,7 @@ impl FileOp {
|
|||
impl FileIoError {
|
||||
pub fn message(&self) -> String {
|
||||
format!(
|
||||
"Failed to {} '{}':<br>{}",
|
||||
"Failed to {} '{}': {}",
|
||||
match &self.op {
|
||||
FileOp::Unknown => return format!("{}", self.source),
|
||||
FileOp::Open => "open".into(),
|
||||
|
|
|
@ -11,7 +11,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
type ResultWithChanges = OpChanges | { changes?: OpChanges };
|
||||
|
||||
export let task: () => Promise<ResultWithChanges | undefined>;
|
||||
export let result: ResultWithChanges | undefined = undefined;
|
||||
export let result: ResultWithChanges | undefined;
|
||||
export let error: Error | undefined;
|
||||
let label: string = "";
|
||||
|
||||
function onUpdate(progress: Progress) {
|
||||
|
@ -24,8 +25,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
$: (async () => {
|
||||
if (!result) {
|
||||
result = await runWithBackendProgress(task, onUpdate);
|
||||
if (!result && !error) {
|
||||
try {
|
||||
result = await runWithBackendProgress(task, onUpdate);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
error = err;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
@ -2,21 +2,34 @@
|
|||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
export interface Choice<T> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Select from "./Select.svelte";
|
||||
import SelectOption from "./SelectOption.svelte";
|
||||
|
||||
export let options: string[] = [];
|
||||
export let disabled: number[] = [];
|
||||
export let value = 0;
|
||||
type T = $$Generic;
|
||||
|
||||
$: label = options[value];
|
||||
export let value: T;
|
||||
export let choices: Choice<T>[] = [];
|
||||
export let disabled: boolean = false;
|
||||
export let disabledChoices: T[] = [];
|
||||
|
||||
$: label = choices.find((c) => c.value === value)?.label;
|
||||
</script>
|
||||
|
||||
<Select bind:value {label}>
|
||||
{#each options as option, idx}
|
||||
<SelectOption value={idx} disabled={disabled.includes(idx)}>
|
||||
{option}
|
||||
<Select bind:value {label} {disabled}>
|
||||
{#each choices as { label: optionLabel, value: optionValue }}
|
||||
<SelectOption
|
||||
value={optionValue}
|
||||
disabled={disabledChoices.includes(optionValue)}
|
||||
>
|
||||
{optionLabel}
|
||||
</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
|
|
|
@ -5,16 +5,19 @@
|
|||
<script lang="ts">
|
||||
import Col from "./Col.svelte";
|
||||
import ConfigInput from "./ConfigInput.svelte";
|
||||
import EnumSelector from "./EnumSelector.svelte";
|
||||
import EnumSelector, { type Choice } from "./EnumSelector.svelte";
|
||||
import RevertButton from "./RevertButton.svelte";
|
||||
import Row from "./Row.svelte";
|
||||
import type { Breakpoint } from "./types";
|
||||
|
||||
export let value: number;
|
||||
export let defaultValue: number;
|
||||
type T = $$Generic;
|
||||
|
||||
export let value: T;
|
||||
export let defaultValue: T;
|
||||
export let breakpoint: Breakpoint = "md";
|
||||
export let choices: string[];
|
||||
export let disabled: number[] = [];
|
||||
export let choices: Choice<T>[];
|
||||
export let disabled: boolean = false;
|
||||
export let disabledChoices: T[] = [];
|
||||
</script>
|
||||
|
||||
<Row --cols={13}>
|
||||
|
@ -23,7 +26,7 @@
|
|||
</Col>
|
||||
<Col --col-size={6} {breakpoint}>
|
||||
<ConfigInput>
|
||||
<EnumSelector bind:value options={choices} {disabled} />
|
||||
<EnumSelector bind:value {choices} {disabled} {disabledChoices} />
|
||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||
</ConfigInput>
|
||||
</Col>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
export let value: boolean;
|
||||
export let defaultValue: boolean;
|
||||
export let disabled: boolean = false;
|
||||
|
||||
const id = Math.random().toString(36).substring(2);
|
||||
</script>
|
||||
|
@ -20,7 +21,7 @@
|
|||
<Col --col-size={4}><Label for={id} preventMouseClick><slot /></Label></Col>
|
||||
<Col --col-justify="flex-end">
|
||||
<ConfigInput grow={false}>
|
||||
<Switch {id} bind:value />
|
||||
<Switch {id} bind:value {disabled} />
|
||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||
</ConfigInput>
|
||||
</Col>
|
||||
|
|
36
ts/components/TagsRow.svelte
Normal file
36
ts/components/TagsRow.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import TagEditor from "../tag-editor/TagEditor.svelte";
|
||||
import Col from "./Col.svelte";
|
||||
import ConfigInput from "./ConfigInput.svelte";
|
||||
import RevertButton from "./RevertButton.svelte";
|
||||
import Row from "./Row.svelte";
|
||||
import type { Breakpoint } from "./types";
|
||||
|
||||
export let tags: string[];
|
||||
export let keyCombination: string | undefined = undefined;
|
||||
export let breakpoint: Breakpoint = "md";
|
||||
|
||||
const tagsWritable = writable<string[]>(tags);
|
||||
</script>
|
||||
|
||||
<Row --cols={13}>
|
||||
<Col --col-size={7} {breakpoint}>
|
||||
<slot />
|
||||
</Col>
|
||||
<Col --col-size={6} {breakpoint}>
|
||||
<ConfigInput>
|
||||
<TagEditor
|
||||
tags={tagsWritable}
|
||||
on:tagsupdate={({ detail }) => (tags = detail.tags)}
|
||||
{keyCombination}
|
||||
/>
|
||||
<RevertButton slot="revert" bind:value={$tagsWritable} defaultValue={[]} />
|
||||
</ConfigInput>
|
||||
</Col>
|
||||
</Row>
|
|
@ -19,8 +19,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import {
|
||||
newGatherPriorityChoices,
|
||||
newSortOrderChoices,
|
||||
reviewMixChoices,
|
||||
reviewOrderChoices,
|
||||
} from "./choices";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import { reviewMixChoices } from "./strings";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
export let api: Record<string, never>;
|
||||
|
@ -30,32 +35,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
const currentDeck = "\n\n" + tr.deckConfigDisplayOrderWillUseCurrentDeck();
|
||||
|
||||
const newGatherPriorityChoices = [
|
||||
tr.deckConfigNewGatherPriorityDeck(),
|
||||
tr.deckConfigNewGatherPriorityPositionLowestFirst(),
|
||||
tr.deckConfigNewGatherPriorityPositionHighestFirst(),
|
||||
tr.deckConfigNewGatherPriorityRandomNotes(),
|
||||
tr.deckConfigNewGatherPriorityRandomCards(),
|
||||
];
|
||||
const newSortOrderChoices = [
|
||||
tr.deckConfigSortOrderTemplateThenGather(),
|
||||
tr.deckConfigSortOrderGather(),
|
||||
tr.deckConfigSortOrderCardTemplateThenRandom(),
|
||||
tr.deckConfigSortOrderRandomNoteThenTemplate(),
|
||||
tr.deckConfigSortOrderRandom(),
|
||||
];
|
||||
const reviewOrderChoices = [
|
||||
tr.deckConfigSortOrderDueDateThenRandom(),
|
||||
tr.deckConfigSortOrderDueDateThenDeck(),
|
||||
tr.deckConfigSortOrderDeckThenDueDate(),
|
||||
tr.deckConfigSortOrderAscendingIntervals(),
|
||||
tr.deckConfigSortOrderDescendingIntervals(),
|
||||
tr.deckConfigSortOrderAscendingEase(),
|
||||
tr.deckConfigSortOrderDescendingEase(),
|
||||
tr.deckConfigSortOrderRelativeOverdueness(),
|
||||
tr.deckConfigSortOrderRandom(),
|
||||
];
|
||||
|
||||
let disabledNewSortOrders: number[] = [];
|
||||
$: {
|
||||
switch ($config.newCardGatherPriority) {
|
||||
|
@ -143,7 +122,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<EnumSelectorRow
|
||||
bind:value={$config.newCardGatherPriority}
|
||||
defaultValue={defaults.newCardGatherPriority}
|
||||
choices={newGatherPriorityChoices}
|
||||
choices={newGatherPriorityChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
|
@ -160,8 +139,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<EnumSelectorRow
|
||||
bind:value={$config.newCardSortOrder}
|
||||
defaultValue={defaults.newCardSortOrder}
|
||||
choices={newSortOrderChoices}
|
||||
disabled={disabledNewSortOrders}
|
||||
choices={newSortOrderChoices()}
|
||||
disabledChoices={disabledNewSortOrders}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
|
@ -212,7 +191,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<EnumSelectorRow
|
||||
bind:value={$config.reviewOrder}
|
||||
defaultValue={defaults.reviewOrder}
|
||||
choices={reviewOrderChoices}
|
||||
choices={reviewOrderChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
|
|
|
@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import { leechChoices } from "./choices";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import StepsInputRow from "./StepsInputRow.svelte";
|
||||
|
@ -37,8 +38,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
: "";
|
||||
}
|
||||
|
||||
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
||||
|
||||
const settings = {
|
||||
relearningSteps: {
|
||||
title: tr.deckConfigRelearningSteps(),
|
||||
|
@ -136,7 +135,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<EnumSelectorRow
|
||||
bind:value={$config.leechAction}
|
||||
defaultValue={defaults.leechAction}
|
||||
choices={leechChoices}
|
||||
choices={leechChoices()}
|
||||
breakpoint="md"
|
||||
>
|
||||
<SettingTitle
|
||||
|
|
|
@ -16,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import { newInsertOrderChoices } from "./choices";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import StepsInputRow from "./StepsInputRow.svelte";
|
||||
|
@ -27,11 +28,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const config = state.currentConfig;
|
||||
const defaults = state.defaults;
|
||||
|
||||
const newInsertOrderChoices = [
|
||||
tr.deckConfigNewInsertionOrderSequential(),
|
||||
tr.deckConfigNewInsertionOrderRandom(),
|
||||
];
|
||||
|
||||
let stepsExceedGraduatingInterval: string;
|
||||
$: {
|
||||
const lastLearnStepInDays = $config.learnSteps.length
|
||||
|
@ -155,7 +151,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<EnumSelectorRow
|
||||
bind:value={$config.newCardInsertOrder}
|
||||
defaultValue={defaults.newCardInsertOrder}
|
||||
choices={newInsertOrderChoices}
|
||||
choices={newInsertOrderChoices()}
|
||||
breakpoint={"md"}
|
||||
>
|
||||
<SettingTitle
|
||||
|
|
147
ts/deck-options/choices.ts
Normal file
147
ts/deck-options/choices.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import {
|
||||
DeckConfig_Config_LeechAction,
|
||||
DeckConfig_Config_NewCardGatherPriority,
|
||||
DeckConfig_Config_NewCardInsertOrder,
|
||||
DeckConfig_Config_NewCardSortOrder,
|
||||
DeckConfig_Config_ReviewCardOrder,
|
||||
DeckConfig_Config_ReviewMix,
|
||||
} from "@tslib/anki/deck_config_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import type { Choice } from "components/EnumSelector.svelte";
|
||||
|
||||
export function newGatherPriorityChoices(): Choice<DeckConfig_Config_NewCardGatherPriority>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.deckConfigNewGatherPriorityDeck(),
|
||||
value: DeckConfig_Config_NewCardGatherPriority.DECK,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigNewGatherPriorityPositionLowestFirst(),
|
||||
value: DeckConfig_Config_NewCardGatherPriority.LOWEST_POSITION,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigNewGatherPriorityPositionHighestFirst(),
|
||||
value: DeckConfig_Config_NewCardGatherPriority.HIGHEST_POSITION,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigNewGatherPriorityRandomNotes(),
|
||||
value: DeckConfig_Config_NewCardGatherPriority.RANDOM_NOTES,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigNewGatherPriorityRandomCards(),
|
||||
value: DeckConfig_Config_NewCardGatherPriority.RANDOM_CARDS,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function newSortOrderChoices(): Choice<DeckConfig_Config_NewCardSortOrder>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.deckConfigSortOrderTemplateThenGather(),
|
||||
value: DeckConfig_Config_NewCardSortOrder.TEMPLATE,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderGather(),
|
||||
value: DeckConfig_Config_NewCardSortOrder.NO_SORT,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderCardTemplateThenRandom(),
|
||||
value: DeckConfig_Config_NewCardSortOrder.TEMPLATE_THEN_RANDOM,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderRandomNoteThenTemplate(),
|
||||
value: DeckConfig_Config_NewCardSortOrder.RANDOM_NOTE_THEN_TEMPLATE,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderRandom(),
|
||||
value: DeckConfig_Config_NewCardSortOrder.RANDOM_CARD,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function reviewOrderChoices(): Choice<DeckConfig_Config_ReviewCardOrder>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.deckConfigSortOrderDueDateThenRandom(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.DAY,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderDueDateThenDeck(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.DAY_THEN_DECK,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderDeckThenDueDate(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.DECK_THEN_DAY,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderAscendingIntervals(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.INTERVALS_ASCENDING,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderDescendingIntervals(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.INTERVALS_DESCENDING,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderAscendingEase(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.EASE_ASCENDING,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderDescendingEase(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.EASE_DESCENDING,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderRelativeOverdueness(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.RELATIVE_OVERDUENESS,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigSortOrderRandom(),
|
||||
value: DeckConfig_Config_ReviewCardOrder.RANDOM,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function reviewMixChoices(): Choice<DeckConfig_Config_ReviewMix>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.deckConfigReviewMixMixWithReviews(),
|
||||
value: DeckConfig_Config_ReviewMix.MIX_WITH_REVIEWS,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigReviewMixShowAfterReviews(),
|
||||
value: DeckConfig_Config_ReviewMix.AFTER_REVIEWS,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigReviewMixShowBeforeReviews(),
|
||||
value: DeckConfig_Config_ReviewMix.BEFORE_REVIEWS,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function leechChoices(): Choice<DeckConfig_Config_LeechAction>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.actionsSuspendCard(),
|
||||
value: DeckConfig_Config_LeechAction.SUSPEND,
|
||||
},
|
||||
{
|
||||
label: tr.schedulingTagOnly(),
|
||||
value: DeckConfig_Config_LeechAction.TAG_ONLY,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function newInsertOrderChoices(): Choice<DeckConfig_Config_NewCardInsertOrder>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.deckConfigNewInsertionOrderSequential(),
|
||||
value: DeckConfig_Config_NewCardInsertOrder.DUE,
|
||||
},
|
||||
{
|
||||
label: tr.deckConfigNewInsertionOrderRandom(),
|
||||
value: DeckConfig_Config_NewCardInsertOrder.RANDOM,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
export const reviewMixChoices = (): string[] => [
|
||||
tr.deckConfigReviewMixMixWithReviews(),
|
||||
tr.deckConfigReviewMixShowAfterReviews(),
|
||||
tr.deckConfigReviewMixShowBeforeReviews(),
|
||||
];
|
|
@ -3,41 +3,26 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ImportAnkiPackageOptions,
|
||||
ImportResponse,
|
||||
} from "@tslib/anki/import_export_pb";
|
||||
import type { ImportAnkiPackageOptions } from "@tslib/anki/import_export_pb";
|
||||
import { importAnkiPackage } from "@tslib/backend";
|
||||
import { importDone } from "@tslib/backend";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { HelpPage } from "@tslib/help-page";
|
||||
import type Carousel from "bootstrap/js/dist/carousel";
|
||||
import type Modal from "bootstrap/js/dist/modal";
|
||||
import BackendProgressIndicator from "components/BackendProgressIndicator.svelte";
|
||||
import Container from "components/Container.svelte";
|
||||
import EnumSelectorRow from "components/EnumSelectorRow.svelte";
|
||||
import Row from "components/Row.svelte";
|
||||
|
||||
import EnumSelectorRow from "../components/EnumSelectorRow.svelte";
|
||||
import HelpModal from "../components/HelpModal.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import StickyHeader from "../components/StickyHeader.svelte";
|
||||
import SwitchRow from "../components/SwitchRow.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import ImportLogPage from "../import-log/ImportLogPage.svelte";
|
||||
import ImportPage from "../import-page/ImportPage.svelte";
|
||||
import { updateChoices } from "./choices";
|
||||
|
||||
export let path: string;
|
||||
export let options: ImportAnkiPackageOptions;
|
||||
|
||||
let importResponse: ImportResponse | undefined = undefined;
|
||||
let importing = false;
|
||||
|
||||
const updateChoices = [
|
||||
tr.importingUpdateIfNewer(),
|
||||
tr.importingUpdateAlways(),
|
||||
tr.importingUpdateNever(),
|
||||
];
|
||||
|
||||
const settings = {
|
||||
mergeNotetypes: {
|
||||
title: tr.importingMergeNotetypes(),
|
||||
|
@ -64,106 +49,75 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let modal: Modal;
|
||||
let carousel: Carousel;
|
||||
|
||||
async function onImport(): Promise<ImportResponse> {
|
||||
const result = await importAnkiPackage({
|
||||
packagePath: path,
|
||||
options,
|
||||
});
|
||||
await importDone({});
|
||||
importing = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
function openHelpModal(index: number): void {
|
||||
modal.show();
|
||||
carousel.to(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if importing}
|
||||
<BackendProgressIndicator task={onImport} bind:result={importResponse} />
|
||||
{:else if importResponse}
|
||||
<ImportLogPage response={importResponse} params={{ path }} />
|
||||
{:else}
|
||||
<StickyHeader {path} onImport={() => (importing = true)} />
|
||||
<ImportPage
|
||||
{path}
|
||||
importer={{
|
||||
doImport: () =>
|
||||
importAnkiPackage({ packagePath: path, options }, { alertOnError: false }),
|
||||
}}
|
||||
>
|
||||
<Row class="d-block">
|
||||
<TitledContainer title={tr.importingImportOptions()}>
|
||||
<HelpModal
|
||||
title={tr.importingImportOptions()}
|
||||
url={HelpPage.PackageImporting.root}
|
||||
slot="tooltip"
|
||||
{helpSections}
|
||||
on:mount={(e) => {
|
||||
modal = e.detail.modal;
|
||||
carousel = e.detail.carousel;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container
|
||||
breakpoint="sm"
|
||||
--gutter-inline="0.25rem"
|
||||
--gutter-block="0.75rem"
|
||||
class="container-columns"
|
||||
>
|
||||
<Row class="d-block">
|
||||
<TitledContainer title={tr.importingImportOptions()}>
|
||||
<HelpModal
|
||||
title={tr.importingImportOptions()}
|
||||
url={HelpPage.PackageImporting.root}
|
||||
slot="tooltip"
|
||||
{helpSections}
|
||||
on:mount={(e) => {
|
||||
modal = e.detail.modal;
|
||||
carousel = e.detail.carousel;
|
||||
}}
|
||||
/>
|
||||
|
||||
<SwitchRow bind:value={options.mergeNotetypes} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf("mergeNotetypes"),
|
||||
)}
|
||||
>
|
||||
{settings.mergeNotetypes.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={options.updateNotes}
|
||||
defaultValue={0}
|
||||
choices={updateChoices}
|
||||
<SwitchRow bind:value={options.mergeNotetypes} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("mergeNotetypes"))}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("updateNotes"))}
|
||||
>
|
||||
{settings.updateNotes.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
{settings.mergeNotetypes.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={options.updateNotetypes}
|
||||
defaultValue={0}
|
||||
choices={updateChoices}
|
||||
<EnumSelectorRow
|
||||
bind:value={options.updateNotes}
|
||||
defaultValue={0}
|
||||
choices={updateChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("updateNotes"))}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf("updateNotetypes"),
|
||||
)}
|
||||
>
|
||||
{settings.updateNotetypes.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
{settings.updateNotes.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
|
||||
<SwitchRow bind:value={options.withScheduling} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf("withScheduling"),
|
||||
)}
|
||||
>
|
||||
{settings.withScheduling.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</TitledContainer>
|
||||
</Row>
|
||||
</Container>
|
||||
{/if}
|
||||
<EnumSelectorRow
|
||||
bind:value={options.updateNotetypes}
|
||||
defaultValue={0}
|
||||
choices={updateChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("updateNotetypes"))}
|
||||
>
|
||||
{settings.updateNotetypes.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.row) {
|
||||
// rows have negative margins by default
|
||||
--bs-gutter-x: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<SwitchRow bind:value={options.withScheduling} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("withScheduling"))}
|
||||
>
|
||||
{settings.withScheduling.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</TitledContainer>
|
||||
</Row>
|
||||
</ImportPage>
|
||||
|
|
23
ts/import-anki-package/choices.ts
Normal file
23
ts/import-anki-package/choices.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { ImportAnkiPackageUpdateCondition } from "@tslib/anki/import_export_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import type { Choice } from "components/EnumSelector.svelte";
|
||||
|
||||
export function updateChoices(): Choice<ImportAnkiPackageUpdateCondition>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.importingUpdateIfNewer(),
|
||||
value: ImportAnkiPackageUpdateCondition.IF_NEWER,
|
||||
},
|
||||
{
|
||||
label: tr.importingUpdateAlways(),
|
||||
value: ImportAnkiPackageUpdateCondition.ALWAYS,
|
||||
},
|
||||
{
|
||||
label: tr.importingUpdateNever(),
|
||||
value: ImportAnkiPackageUpdateCondition.NEVER,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -18,9 +18,6 @@
|
|||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
width: min(100vw, 70em);
|
||||
margin: 0 auto;
|
||||
padding: 0 1em 1em 1em;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CsvMetadata_MatchScope } from "@tslib/anki/import_export_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Select from "../components/Select.svelte";
|
||||
import SelectOption from "../components/SelectOption.svelte";
|
||||
|
||||
export let matchScope: CsvMetadata_MatchScope;
|
||||
|
||||
const matchScopes = [
|
||||
{
|
||||
value: CsvMetadata_MatchScope.NOTETYPE,
|
||||
label: tr.notetypesNotetype(),
|
||||
},
|
||||
{
|
||||
value: CsvMetadata_MatchScope.NOTETYPE_AND_DECK,
|
||||
label: tr.importingNotetypeAndDeck(),
|
||||
},
|
||||
];
|
||||
|
||||
$: label = matchScopes.find((r) => r.value === matchScope)?.label;
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.importingMatchScope()}
|
||||
</Col>
|
||||
<Col --col-size={1}>
|
||||
<Select bind:value={matchScope} {label}>
|
||||
{#each matchScopes as { label, value }}
|
||||
<SelectOption {value}>{label}</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
|
@ -1,31 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { DeckNameId } from "@tslib/anki/decks_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Select from "../components/Select.svelte";
|
||||
import SelectOption from "../components/SelectOption.svelte";
|
||||
|
||||
export let deckNameIds: DeckNameId[];
|
||||
export let deckId: bigint;
|
||||
|
||||
$: label = deckNameIds.find((d) => d.id === deckId)?.name.replace(/^.+::/, "...");
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.decksDeck()}
|
||||
</Col>
|
||||
<Col --col-size={1}>
|
||||
<Select bind:value={deckId} {label}>
|
||||
{#each deckNameIds as { id, name }}
|
||||
<SelectOption value={id}>{name}</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
|
@ -1,40 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CsvMetadata_Delimiter as Delimiter } from "@tslib/anki/import_export_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Select from "../components/Select.svelte";
|
||||
import SelectOption from "../components/SelectOption.svelte";
|
||||
|
||||
export let delimiter: Delimiter;
|
||||
export let disabled: boolean;
|
||||
|
||||
const delimiters = [
|
||||
{ value: Delimiter.TAB, label: tr.importingTab() },
|
||||
{ value: Delimiter.PIPE, label: tr.importingPipe() },
|
||||
{ value: Delimiter.SEMICOLON, label: tr.importingSemicolon() },
|
||||
{ value: Delimiter.COLON, label: tr.importingColon() },
|
||||
{ value: Delimiter.COMMA, label: tr.importingComma() },
|
||||
{ value: Delimiter.SPACE, label: tr.studyingSpace() },
|
||||
];
|
||||
|
||||
$: label = delimiters.find((d) => d.value === delimiter)?.label;
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.importingFieldSeparator()}
|
||||
</Col>
|
||||
<Col --col-size={1}>
|
||||
<Select bind:value={delimiter} {disabled} {label}>
|
||||
{#each delimiters as { value, label }}
|
||||
<SelectOption {value}>{label}</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
|
@ -1,45 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CsvMetadata_DupeResolution as DupeResolution } from "@tslib/anki/import_export_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Select from "../components/Select.svelte";
|
||||
import SelectOption from "../components/SelectOption.svelte";
|
||||
|
||||
export let dupeResolution: DupeResolution;
|
||||
|
||||
const dupeResolutions = [
|
||||
{
|
||||
value: DupeResolution.UPDATE,
|
||||
label: tr.importingUpdate(),
|
||||
},
|
||||
{
|
||||
value: DupeResolution.DUPLICATE,
|
||||
label: tr.importingDuplicate(),
|
||||
},
|
||||
{
|
||||
value: DupeResolution.PRESERVE,
|
||||
label: tr.importingPreserve(),
|
||||
},
|
||||
];
|
||||
|
||||
$: label = dupeResolutions.find((r) => r.value === dupeResolution)?.label;
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.importingExistingNotes()}
|
||||
</Col>
|
||||
<Col --col-size={1}>
|
||||
<Select bind:value={dupeResolution} {label}>
|
||||
{#each dupeResolutions as { label, value }}
|
||||
<SelectOption {value}>{label}</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
|
@ -3,41 +3,36 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { CsvMetadata_MappedNotetype } from "@tslib/anki/import_export_pb";
|
||||
import { getFieldNames } from "@tslib/backend";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import TitledContainer from "components/TitledContainer.svelte";
|
||||
|
||||
import Spacer from "../components/Spacer.svelte";
|
||||
import type { ColumnOption } from "./lib";
|
||||
import type { ImportCsvState } from "./lib";
|
||||
import MapperRow from "./MapperRow.svelte";
|
||||
|
||||
export let columnOptions: ColumnOption[];
|
||||
export let tagsColumn: number;
|
||||
export let globalNotetype: CsvMetadata_MappedNotetype | null;
|
||||
export let state: ImportCsvState;
|
||||
|
||||
let lastNotetypeId: bigint | undefined = -1n;
|
||||
let fieldNamesPromise: Promise<string[]>;
|
||||
|
||||
$: if (globalNotetype?.id !== lastNotetypeId) {
|
||||
lastNotetypeId = globalNotetype?.id;
|
||||
fieldNamesPromise =
|
||||
globalNotetype === null
|
||||
? Promise.resolve([])
|
||||
: getFieldNames({ ntid: globalNotetype.id }).then((list) => list.vals);
|
||||
}
|
||||
const metadata = state.metadata;
|
||||
const globalNotetype = state.globalNotetype;
|
||||
const fieldNamesPromise = state.fieldNames;
|
||||
const columnOptions = state.columnOptions;
|
||||
</script>
|
||||
|
||||
{#if globalNotetype}
|
||||
{#await fieldNamesPromise then fieldNames}
|
||||
{#each fieldNames as label, idx}
|
||||
<!-- first index is treated specially, because it must be assigned some column -->
|
||||
<MapperRow
|
||||
{label}
|
||||
columnOptions={idx === 0 ? columnOptions.slice(1) : columnOptions}
|
||||
bind:value={globalNotetype.fieldColumns[idx]}
|
||||
/>
|
||||
{/each}
|
||||
{/await}
|
||||
<Spacer --height="1.5rem" />
|
||||
{/if}
|
||||
<MapperRow label={tr.editingTags()} {columnOptions} bind:value={tagsColumn} />
|
||||
<TitledContainer title={tr.importingFieldMapping()}>
|
||||
{#if $globalNotetype !== null}
|
||||
{#await $fieldNamesPromise then fieldNames}
|
||||
{#each fieldNames as label, idx}
|
||||
<!-- first index is treated specially, because it must be assigned some column -->
|
||||
<MapperRow
|
||||
{label}
|
||||
columnOptions={idx === 0 ? $columnOptions.slice(1) : $columnOptions}
|
||||
bind:value={$globalNotetype.fieldColumns[idx]}
|
||||
/>
|
||||
{/each}
|
||||
{/await}
|
||||
{/if}
|
||||
<MapperRow
|
||||
label={tr.editingTags()}
|
||||
columnOptions={$columnOptions}
|
||||
bind:value={$metadata.tagsColumn}
|
||||
/>
|
||||
</TitledContainer>
|
||||
|
|
84
ts/import-csv/FileOptions.svelte
Normal file
84
ts/import-csv/FileOptions.svelte
Normal file
|
@ -0,0 +1,84 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { HelpPage } from "@tslib/help-page";
|
||||
import type Carousel from "bootstrap/js/dist/carousel";
|
||||
import type Modal from "bootstrap/js/dist/modal";
|
||||
|
||||
import EnumSelectorRow from "../components/EnumSelectorRow.svelte";
|
||||
import HelpModal from "../components/HelpModal.svelte";
|
||||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import SwitchRow from "../components/SwitchRow.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import { delimiterChoices } from "./choices";
|
||||
import type { ImportCsvState } from "./lib";
|
||||
import Preview from "./Preview.svelte";
|
||||
|
||||
export let state: ImportCsvState;
|
||||
|
||||
const metadata = state.metadata;
|
||||
|
||||
const settings = {
|
||||
delimiter: {
|
||||
title: tr.importingFieldSeparator(),
|
||||
help: tr.importingFieldSeparatorHelp(),
|
||||
url: HelpPage.TextImporting.root,
|
||||
},
|
||||
isHtml: {
|
||||
title: tr.importingAllowHtmlInFields(),
|
||||
help: tr.importingAllowHtmlInFieldsHelp(),
|
||||
url: HelpPage.TextImporting.html,
|
||||
},
|
||||
};
|
||||
const helpSections = Object.values(settings) as HelpItem[];
|
||||
let modal: Modal;
|
||||
let carousel: Carousel;
|
||||
|
||||
function openHelpModal(index: number): void {
|
||||
modal.show();
|
||||
carousel.to(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.importingFile()}>
|
||||
<HelpModal
|
||||
title={tr.importingFile()}
|
||||
url={HelpPage.TextImporting.root}
|
||||
slot="tooltip"
|
||||
{helpSections}
|
||||
on:mount={(e) => {
|
||||
modal = e.detail.modal;
|
||||
carousel = e.detail.carousel;
|
||||
}}
|
||||
/>
|
||||
<EnumSelectorRow
|
||||
bind:value={$metadata.delimiter}
|
||||
defaultValue={state.defaultDelimiter}
|
||||
choices={delimiterChoices()}
|
||||
disabled={$metadata.forceDelimiter}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("delimiter"))}
|
||||
>
|
||||
{settings.delimiter.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
|
||||
<SwitchRow
|
||||
bind:value={$metadata.isHtml}
|
||||
defaultValue={state.defaultIsHtml}
|
||||
disabled={$metadata.forceIsHtml}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("isHtml"))}
|
||||
>
|
||||
{settings.isHtml.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
|
||||
<Preview {state} />
|
||||
</TitledContainer>
|
|
@ -1,17 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let heading: string;
|
||||
</script>
|
||||
|
||||
<h1>
|
||||
{heading}
|
||||
</h1>
|
||||
|
||||
<style lang="scss">
|
||||
h1 {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
</style>
|
|
@ -1,23 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Switch from "../components/Switch.svelte";
|
||||
|
||||
export let isHtml: boolean;
|
||||
export let disabled: boolean;
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.importingAllowHtmlInFields()}
|
||||
</Col>
|
||||
<Col --col-size={1} --col-justify="flex-end">
|
||||
<Switch id={undefined} bind:value={isHtml} {disabled} />
|
||||
</Col>
|
||||
</Row>
|
|
@ -3,192 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { DeckNameId } from "@tslib/anki/decks_pb";
|
||||
import type { StringList } from "@tslib/anki/generic_pb";
|
||||
import type {
|
||||
CsvMetadata_Delimiter,
|
||||
CsvMetadata_DupeResolution,
|
||||
CsvMetadata_MappedNotetype,
|
||||
CsvMetadata_MatchScope,
|
||||
ImportResponse,
|
||||
} from "@tslib/anki/import_export_pb";
|
||||
import type { NotetypeNameId } from "@tslib/anki/notetypes_pb";
|
||||
import { getCsvMetadata, importCsv, importDone } from "@tslib/backend";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import BackendProgressIndicator from "../components/BackendProgressIndicator.svelte";
|
||||
import Col from "../components/Col.svelte";
|
||||
import Container from "../components/Container.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Spacer from "../components/Spacer.svelte";
|
||||
import StickyHeader from "../components/StickyHeader.svelte";
|
||||
import ImportLogPage from "../import-log/ImportLogPage.svelte";
|
||||
import DeckDupeCheckSwitch from "./DeckDupeCheckSwitch.svelte";
|
||||
import DeckSelector from "./DeckSelector.svelte";
|
||||
import DelimiterSelector from "./DelimiterSelector.svelte";
|
||||
import DupeResolutionSelector from "./DupeResolutionSelector.svelte";
|
||||
import ImportPage from "../import-page/ImportPage.svelte";
|
||||
import FieldMapper from "./FieldMapper.svelte";
|
||||
import Header from "./Header.svelte";
|
||||
import HtmlSwitch from "./HtmlSwitch.svelte";
|
||||
import {
|
||||
buildDeckOneof,
|
||||
buildNotetypeOneof,
|
||||
getColumnOptions,
|
||||
tryGetDeckId,
|
||||
tryGetGlobalNotetype,
|
||||
} from "./lib";
|
||||
import NotetypeSelector from "./NotetypeSelector.svelte";
|
||||
import Preview from "./Preview.svelte";
|
||||
import Tags from "./Tags.svelte";
|
||||
import FileOptions from "./FileOptions.svelte";
|
||||
import ImportOptions from "./ImportOptions.svelte";
|
||||
import type { ImportCsvState } from "./lib";
|
||||
|
||||
export let path: string;
|
||||
export let notetypeNameIds: NotetypeNameId[];
|
||||
export let deckNameIds: DeckNameId[];
|
||||
export let dupeResolution: CsvMetadata_DupeResolution;
|
||||
export let matchScope: CsvMetadata_MatchScope;
|
||||
export let delimiter: CsvMetadata_Delimiter;
|
||||
export let forceDelimiter: boolean;
|
||||
export let forceIsHtml: boolean;
|
||||
export let isHtml: boolean;
|
||||
export let globalTags: string[];
|
||||
export let updatedTags: string[];
|
||||
export let columnLabels: string[];
|
||||
export let tagsColumn: number;
|
||||
export let guidColumn: number;
|
||||
export let preview: StringList[];
|
||||
// Protobuf oneofs. Exactly one of these pairs is expected to be set.
|
||||
export let notetypeColumn: number | null;
|
||||
export let globalNotetype: CsvMetadata_MappedNotetype | null;
|
||||
export let deckId: bigint | null;
|
||||
export let deckColumn: number | null;
|
||||
|
||||
let importResponse: ImportResponse | undefined = undefined;
|
||||
let lastNotetypeId = globalNotetype?.id;
|
||||
let lastDelimeter = delimiter;
|
||||
let importing = false;
|
||||
|
||||
$: columnOptions = getColumnOptions(
|
||||
columnLabels,
|
||||
preview[0].vals,
|
||||
notetypeColumn,
|
||||
deckColumn,
|
||||
guidColumn,
|
||||
);
|
||||
$: getCsvMetadata({
|
||||
path,
|
||||
delimiter,
|
||||
notetypeId: undefined,
|
||||
deckId: undefined,
|
||||
isHtml,
|
||||
}).then((meta) => {
|
||||
columnLabels = meta.columnLabels;
|
||||
preview = meta.preview;
|
||||
});
|
||||
$: if (globalNotetype?.id !== lastNotetypeId || delimiter !== lastDelimeter) {
|
||||
lastNotetypeId = globalNotetype?.id;
|
||||
lastDelimeter = delimiter;
|
||||
getCsvMetadata({
|
||||
path,
|
||||
delimiter,
|
||||
notetypeId: globalNotetype?.id,
|
||||
deckId: deckId ?? undefined,
|
||||
}).then((meta) => {
|
||||
globalNotetype = tryGetGlobalNotetype(meta);
|
||||
deckId = tryGetDeckId(meta);
|
||||
tagsColumn = meta.tagsColumn;
|
||||
});
|
||||
}
|
||||
|
||||
async function onImport(): Promise<ImportResponse> {
|
||||
const result = await importCsv({
|
||||
path,
|
||||
metadata: {
|
||||
dupeResolution,
|
||||
matchScope,
|
||||
delimiter,
|
||||
forceDelimiter,
|
||||
isHtml,
|
||||
forceIsHtml,
|
||||
globalTags,
|
||||
updatedTags,
|
||||
columnLabels,
|
||||
tagsColumn,
|
||||
guidColumn,
|
||||
deck: buildDeckOneof(deckColumn, deckId),
|
||||
notetype: buildNotetypeOneof(globalNotetype, notetypeColumn),
|
||||
preview: [],
|
||||
},
|
||||
});
|
||||
await importDone({});
|
||||
importing = false;
|
||||
return result;
|
||||
}
|
||||
export let state: ImportCsvState;
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
{#if importing}
|
||||
<BackendProgressIndicator task={onImport} bind:result={importResponse} />
|
||||
{:else if importResponse}
|
||||
<ImportLogPage response={importResponse} params={{ path }} />
|
||||
{:else}
|
||||
<StickyHeader {path} onImport={() => (importing = true)} />
|
||||
|
||||
<Container class="csv-page">
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1} breakpoint="md">
|
||||
<Container>
|
||||
<Header heading={tr.importingFile()} />
|
||||
<Spacer --height="1.5rem" />
|
||||
<DelimiterSelector bind:delimiter disabled={forceDelimiter} />
|
||||
<HtmlSwitch bind:isHtml disabled={forceIsHtml} />
|
||||
<Preview {columnOptions} {preview} />
|
||||
</Container>
|
||||
</Col>
|
||||
<Col --col-size={1} breakpoint="md">
|
||||
<Container>
|
||||
<Header heading={tr.importingImportOptions()} />
|
||||
<Spacer --height="1.5rem" />
|
||||
{#if globalNotetype}
|
||||
<NotetypeSelector
|
||||
{notetypeNameIds}
|
||||
bind:notetypeId={globalNotetype.id}
|
||||
/>
|
||||
{/if}
|
||||
{#if deckId}
|
||||
<DeckSelector {deckNameIds} bind:deckId />
|
||||
{/if}
|
||||
<DupeResolutionSelector bind:dupeResolution />
|
||||
<DeckDupeCheckSwitch bind:matchScope />
|
||||
<Tags bind:globalTags bind:updatedTags />
|
||||
</Container>
|
||||
</Col>
|
||||
<Col --col-size={1} breakpoint="md">
|
||||
<Container>
|
||||
<Header heading={tr.importingFieldMapping()} />
|
||||
<Spacer --height="1.5rem" />
|
||||
<FieldMapper
|
||||
{columnOptions}
|
||||
bind:globalNotetype
|
||||
bind:tagsColumn
|
||||
/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.outer {
|
||||
margin: 0 auto;
|
||||
}
|
||||
:global(.csv-page) {
|
||||
--gutter-inline: 0.25rem;
|
||||
|
||||
:global(.row) {
|
||||
// rows have negative margins by default
|
||||
--bs-gutter-x: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ImportPage path={state.path} importer={state}>
|
||||
<Row><FileOptions {state} /></Row>
|
||||
<Row><ImportOptions {state} /></Row>
|
||||
<Row><FieldMapper {state} /></Row>
|
||||
</ImportPage>
|
||||
|
|
153
ts/import-csv/ImportOptions.svelte
Normal file
153
ts/import-csv/ImportOptions.svelte
Normal file
|
@ -0,0 +1,153 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { HelpPage } from "@tslib/help-page";
|
||||
import type Carousel from "bootstrap/js/dist/carousel";
|
||||
import type Modal from "bootstrap/js/dist/modal";
|
||||
|
||||
import EnumSelectorRow from "../components/EnumSelectorRow.svelte";
|
||||
import HelpModal from "../components/HelpModal.svelte";
|
||||
import SettingTitle from "../components/SettingTitle.svelte";
|
||||
import TagsRow from "../components/TagsRow.svelte";
|
||||
import TitledContainer from "../components/TitledContainer.svelte";
|
||||
import type { HelpItem } from "../components/types";
|
||||
import { dupeResolutionChoices, matchScopeChoices } from "./choices";
|
||||
import type { ImportCsvState } from "./lib";
|
||||
|
||||
export let state: ImportCsvState;
|
||||
|
||||
const metadata = state.metadata;
|
||||
const globalNotetype = state.globalNotetype;
|
||||
const deckId = state.deckId;
|
||||
|
||||
const settings = {
|
||||
notetype: {
|
||||
title: tr.notetypesNotetype(),
|
||||
help: tr.importingNotetypeHelp(),
|
||||
url: HelpPage.TextImporting.root,
|
||||
},
|
||||
deck: {
|
||||
title: tr.decksDeck(),
|
||||
help: tr.importingDeckHelp(),
|
||||
url: HelpPage.TextImporting.root,
|
||||
},
|
||||
dupeResolution: {
|
||||
title: tr.importingExistingNotes(),
|
||||
help: tr.importingExistingNotesHelp(),
|
||||
url: HelpPage.TextImporting.updating,
|
||||
},
|
||||
matchScope: {
|
||||
title: tr.importingMatchScope(),
|
||||
help: tr.importingMatchScopeHelp(),
|
||||
url: HelpPage.TextImporting.updating,
|
||||
},
|
||||
globalTags: {
|
||||
title: tr.importingTagAllNotes(),
|
||||
help: tr.importingTagAllNotesHelp(),
|
||||
url: HelpPage.TextImporting.root,
|
||||
},
|
||||
updatedTags: {
|
||||
title: tr.importingTagUpdatedNotes(),
|
||||
help: tr.importingTagUpdatedNotesHelp(),
|
||||
url: HelpPage.TextImporting.root,
|
||||
},
|
||||
};
|
||||
const helpSections = Object.values(settings) as HelpItem[];
|
||||
let modal: Modal;
|
||||
let carousel: Carousel;
|
||||
|
||||
function openHelpModal(index: number): void {
|
||||
modal.show();
|
||||
carousel.to(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.importingImportOptions()}>
|
||||
<HelpModal
|
||||
title={tr.importingImportOptions()}
|
||||
url={HelpPage.TextImporting.root}
|
||||
slot="tooltip"
|
||||
{helpSections}
|
||||
on:mount={(e) => {
|
||||
modal = e.detail.modal;
|
||||
carousel = e.detail.carousel;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if $globalNotetype !== null}
|
||||
<EnumSelectorRow
|
||||
bind:value={$globalNotetype.id}
|
||||
defaultValue={state.defaultNotetypeId}
|
||||
choices={state.notetypeNameIds.map(({ id, name }) => {
|
||||
return { label: name, value: id };
|
||||
})}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("notetype"))}
|
||||
>
|
||||
{settings.notetype.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
{/if}
|
||||
|
||||
{#if $deckId !== null}
|
||||
<EnumSelectorRow
|
||||
bind:value={$deckId}
|
||||
defaultValue={state.defaultDeckId}
|
||||
choices={state.deckNameIds.map(({ id, name }) => {
|
||||
return { label: name, value: id };
|
||||
})}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("deck"))}
|
||||
>
|
||||
{settings.deck.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
{/if}
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={$metadata.dupeResolution}
|
||||
defaultValue={0}
|
||||
choices={dupeResolutionChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("dupeResolution"))}
|
||||
>
|
||||
{settings.dupeResolution.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={$metadata.matchScope}
|
||||
defaultValue={0}
|
||||
choices={matchScopeChoices()}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("matchScope"))}
|
||||
>
|
||||
{settings.matchScope.title}
|
||||
</SettingTitle>
|
||||
</EnumSelectorRow>
|
||||
|
||||
<TagsRow bind:tags={$metadata.globalTags} keyCombination={"Control+T"}>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("globalTags"))}
|
||||
>
|
||||
{settings.globalTags.title}
|
||||
</SettingTitle>
|
||||
</TagsRow>
|
||||
|
||||
<TagsRow bind:tags={$metadata.updatedTags} keyCombination={"Control+Shift+T"}>
|
||||
<SettingTitle
|
||||
on:click={() => openHelpModal(Object.keys(settings).indexOf("updatedTags"))}
|
||||
>
|
||||
{settings.updatedTags.title}
|
||||
</SettingTitle>
|
||||
</TagsRow>
|
||||
</TitledContainer>
|
|
@ -1,31 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { NotetypeNameId } from "@tslib/anki/notetypes_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import Select from "../components/Select.svelte";
|
||||
import SelectOption from "../components/SelectOption.svelte";
|
||||
|
||||
export let notetypeNameIds: NotetypeNameId[];
|
||||
export let notetypeId: bigint;
|
||||
|
||||
$: label = notetypeNameIds.find((n) => n.id === notetypeId)?.name;
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col --col-size={1}>
|
||||
{tr.notetypesNotetype()}
|
||||
</Col>
|
||||
<Col --col-size={1}>
|
||||
<Select bind:value={notetypeId} {label}>
|
||||
{#each notetypeNameIds as { id, name }}
|
||||
<SelectOption value={id}>{name}</SelectOption>
|
||||
{/each}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
|
@ -3,22 +3,22 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { StringList } from "@tslib/anki/generic_pb";
|
||||
import { type ImportCsvState } from "./lib";
|
||||
|
||||
import type { ColumnOption } from "./lib";
|
||||
export let state: ImportCsvState;
|
||||
|
||||
export let columnOptions: ColumnOption[];
|
||||
export let preview: StringList[];
|
||||
const metadata = state.metadata;
|
||||
const columnOptions = state.columnOptions;
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<table class="preview">
|
||||
{#each columnOptions.slice(1) as { label, shortLabel }}
|
||||
{#each $columnOptions.slice(1) as { label, shortLabel }}
|
||||
<th>
|
||||
{shortLabel || label}
|
||||
</th>
|
||||
{/each}
|
||||
{#each preview as row}
|
||||
{#each $metadata.preview as row}
|
||||
<tr>
|
||||
{#each row.vals as cell}
|
||||
<td>{cell}</td>
|
||||
|
@ -30,8 +30,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<style lang="scss">
|
||||
.outer {
|
||||
// approximate size based on body max width + margins
|
||||
width: min(90vw, 65em);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "@tslib/ftl";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import Col from "../components/Col.svelte";
|
||||
import Row from "../components/Row.svelte";
|
||||
import TagEditor from "../tag-editor/TagEditor.svelte";
|
||||
|
||||
export let globalTags: string[];
|
||||
export let updatedTags: string[];
|
||||
|
||||
const globalTagsWritable = writable<string[]>(globalTags);
|
||||
const updatedTagsWritable = writable<string[]>(updatedTags);
|
||||
</script>
|
||||
|
||||
<Row --cols={2}>
|
||||
<Col>{tr.importingTagAllNotes()}</Col>
|
||||
<Col>
|
||||
<TagEditor
|
||||
tags={globalTagsWritable}
|
||||
on:tagsupdate={({ detail }) => (globalTags = detail.tags)}
|
||||
keyCombination={"Control+T"}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row --cols={2}>
|
||||
<Col>{tr.importingTagUpdatedNotes()}</Col>
|
||||
<Col>
|
||||
<TagEditor
|
||||
tags={updatedTagsWritable}
|
||||
on:tagsupdate={({ detail }) => (updatedTags = detail.tags)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
54
ts/import-csv/choices.ts
Normal file
54
ts/import-csv/choices.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import {
|
||||
CsvMetadata_Delimiter,
|
||||
CsvMetadata_DupeResolution,
|
||||
CsvMetadata_MatchScope,
|
||||
} from "@tslib/anki/import_export_pb";
|
||||
import * as tr from "@tslib/ftl";
|
||||
import type { Choice } from "components/EnumSelector.svelte";
|
||||
|
||||
export function delimiterChoices(): Choice<CsvMetadata_Delimiter>[] {
|
||||
return [
|
||||
{
|
||||
label: tr.importingTab(),
|
||||
value: CsvMetadata_Delimiter.TAB,
|
||||
},
|
||||
{
|
||||
label: tr.importingPipe(),
|
||||
value: CsvMetadata_Delimiter.PIPE,
|
||||
},
|
||||
{
|
||||
label: tr.importingSemicolon(),
|
||||
value: CsvMetadata_Delimiter.SEMICOLON,
|
||||
},
|
||||
{
|
||||
label: tr.importingColon(),
|
||||
value: CsvMetadata_Delimiter.COLON,
|
||||
},
|
||||
{
|
||||
label: tr.importingComma(),
|
||||
value: CsvMetadata_Delimiter.COMMA,
|
||||
},
|
||||
{
|
||||
label: tr.studyingSpace(),
|
||||
value: CsvMetadata_Delimiter.SPACE,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function dupeResolutionChoices(): Choice<CsvMetadata_DupeResolution>[] {
|
||||
return [
|
||||
{ label: tr.importingUpdate(), value: CsvMetadata_DupeResolution.UPDATE },
|
||||
{ label: tr.importingPreserve(), value: CsvMetadata_DupeResolution.PRESERVE },
|
||||
{ label: tr.importingDuplicate(), value: CsvMetadata_DupeResolution.DUPLICATE },
|
||||
];
|
||||
}
|
||||
|
||||
export function matchScopeChoices(): Choice<CsvMetadata_MatchScope>[] {
|
||||
return [
|
||||
{ label: tr.notetypesNotetype(), value: CsvMetadata_MatchScope.NOTETYPE },
|
||||
{ label: tr.importingNotetypeAndDeck(), value: CsvMetadata_MatchScope.NOTETYPE_AND_DECK },
|
||||
];
|
||||
}
|
|
@ -7,6 +7,9 @@
|
|||
@import "bootstrap/scss/button-group";
|
||||
@import "bootstrap/scss/close";
|
||||
@import "bootstrap/scss/grid";
|
||||
@import "bootstrap/scss/transitions";
|
||||
@import "bootstrap/scss/modal";
|
||||
@import "bootstrap/scss/carousel";
|
||||
@import "sass/bootstrap-forms";
|
||||
@import "sass/bootstrap-tooltip";
|
||||
|
||||
|
|
|
@ -7,9 +7,10 @@ import { getCsvMetadata, getDeckNames, getNotetypeNames } from "@tslib/backend";
|
|||
import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||
import { checkNightMode } from "@tslib/nightmode";
|
||||
|
||||
import { modalsKey } from "../components/context-keys";
|
||||
import ErrorPage from "../components/ErrorPage.svelte";
|
||||
import ImportCsvPage from "./ImportCsvPage.svelte";
|
||||
import { tryGetDeckColumn, tryGetDeckId, tryGetGlobalNotetype, tryGetNotetypeColumn } from "./lib";
|
||||
import { ImportCsvState } from "./lib";
|
||||
|
||||
const i18n = setupI18n({
|
||||
modules: [
|
||||
|
@ -22,11 +23,16 @@ const i18n = setupI18n({
|
|||
ModuleName.NOTETYPES,
|
||||
ModuleName.STUDYING,
|
||||
ModuleName.ADDING,
|
||||
ModuleName.HELP,
|
||||
ModuleName.DECK_CONFIG,
|
||||
],
|
||||
});
|
||||
|
||||
export async function setupImportCsvPage(path: string): Promise<ImportCsvPage | ErrorPage> {
|
||||
const context = new Map();
|
||||
context.set(modalsKey, new Map());
|
||||
checkNightMode();
|
||||
|
||||
return Promise.all([
|
||||
getNotetypeNames({}),
|
||||
getDeckNames({
|
||||
|
@ -35,32 +41,13 @@ export async function setupImportCsvPage(path: string): Promise<ImportCsvPage |
|
|||
}),
|
||||
getCsvMetadata({ path }, { alertOnError: false }),
|
||||
i18n,
|
||||
]).then(([notetypes, decks, metadata, _i18n]) => {
|
||||
]).then(([notetypes, decks, metadata]) => {
|
||||
return new ImportCsvPage({
|
||||
target: document.body,
|
||||
props: {
|
||||
path: path,
|
||||
deckNameIds: decks.entries,
|
||||
notetypeNameIds: notetypes.entries,
|
||||
dupeResolution: metadata.dupeResolution,
|
||||
matchScope: metadata.matchScope,
|
||||
delimiter: metadata.delimiter,
|
||||
forceDelimiter: metadata.forceDelimiter,
|
||||
isHtml: metadata.isHtml,
|
||||
forceIsHtml: metadata.forceIsHtml,
|
||||
globalTags: metadata.globalTags,
|
||||
updatedTags: metadata.updatedTags,
|
||||
columnLabels: metadata.columnLabels,
|
||||
tagsColumn: metadata.tagsColumn,
|
||||
guidColumn: metadata.guidColumn,
|
||||
preview: metadata.preview,
|
||||
globalNotetype: tryGetGlobalNotetype(metadata),
|
||||
// Unset oneof numbers default to 0, which also means n/a here,
|
||||
// but it's vital to differentiate between unset and 0 when reserializing.
|
||||
notetypeColumn: tryGetNotetypeColumn(metadata),
|
||||
deckId: tryGetDeckId(metadata),
|
||||
deckColumn: tryGetDeckColumn(metadata),
|
||||
state: new ImportCsvState(path, notetypes, decks, metadata),
|
||||
},
|
||||
context,
|
||||
});
|
||||
}).catch((error) => {
|
||||
return new ErrorPage({ target: document.body, props: { error } });
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { CsvMetadata, CsvMetadata_MappedNotetype } from "@tslib/anki/import_export_pb";
|
||||
import type { DeckNameId, DeckNames } from "@tslib/anki/decks_pb";
|
||||
import type { CsvMetadata, CsvMetadata_Delimiter, ImportResponse } from "@tslib/anki/import_export_pb";
|
||||
import { type CsvMetadata_MappedNotetype } from "@tslib/anki/import_export_pb";
|
||||
import type { NotetypeNameId, NotetypeNames } from "@tslib/anki/notetypes_pb";
|
||||
import { getCsvMetadata, getFieldNames, importCsv } from "@tslib/backend";
|
||||
import * as tr from "@tslib/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;
|
||||
|
@ -11,24 +18,164 @@ export interface ColumnOption {
|
|||
disabled: boolean;
|
||||
}
|
||||
|
||||
export function getColumnOptions(
|
||||
columnLabels: string[],
|
||||
firstRow: string[],
|
||||
notetypeColumn: number | null,
|
||||
deckColumn: number | null,
|
||||
guidColumn: number,
|
||||
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(
|
||||
columnLabels.map((label, index) => {
|
||||
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 === guidColumn) {
|
||||
} else if (index === metadata.guidColumn) {
|
||||
return columnOption("GUID", true, index);
|
||||
} else if (label === "") {
|
||||
return columnOption(firstRow[index - 1], false, index, true);
|
||||
return columnOption(metadata.preview[0].vals[index - 1], false, index, true);
|
||||
} else {
|
||||
return columnOption(label, false, index);
|
||||
}
|
||||
|
@ -50,42 +197,10 @@ function columnOption(
|
|||
};
|
||||
}
|
||||
|
||||
export function tryGetGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype | null {
|
||||
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
||||
}
|
||||
|
||||
export function tryGetDeckId(meta: CsvMetadata): bigint | null {
|
||||
return meta.deck.case === "deckId" ? meta.deck.value : null;
|
||||
}
|
||||
|
||||
export function tryGetDeckColumn(meta: CsvMetadata): number | null {
|
||||
function getDeckColumn(meta: CsvMetadata): number | null {
|
||||
return meta.deck.case === "deckColumn" ? meta.deck.value : null;
|
||||
}
|
||||
|
||||
export function tryGetNotetypeColumn(meta: CsvMetadata): number | null {
|
||||
function getNotetypeColumn(meta: CsvMetadata): number | null {
|
||||
return meta.notetype.case === "notetypeColumn" ? meta.notetype.value : null;
|
||||
}
|
||||
|
||||
export function buildDeckOneof(
|
||||
deckColumn: number | null,
|
||||
deckId: bigint | null,
|
||||
): CsvMetadata["deck"] {
|
||||
if (deckColumn !== null) {
|
||||
return { case: "deckColumn", value: deckColumn };
|
||||
} else if (deckId !== null) {
|
||||
return { case: "deckId", value: deckId };
|
||||
}
|
||||
throw new Error("missing column/id");
|
||||
}
|
||||
|
||||
export function buildNotetypeOneof(
|
||||
globalNotetype: CsvMetadata_MappedNotetype | null,
|
||||
notetypeColumn: number | null,
|
||||
): CsvMetadata["notetype"] {
|
||||
if (globalNotetype !== null) {
|
||||
return { case: "globalNotetype", value: globalNotetype };
|
||||
} else if (notetypeColumn !== null) {
|
||||
return { case: "notetypeColumn", value: notetypeColumn };
|
||||
}
|
||||
throw new Error("missing column/id");
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import "./import-log-base.scss";
|
||||
|
||||
import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||
import { checkNightMode } from "@tslib/nightmode";
|
||||
|
||||
import ImportLogPage from "./ImportLogPage.svelte";
|
||||
import type { LogParams } from "./types";
|
||||
|
||||
const i18n = setupI18n({
|
||||
modules: [
|
||||
ModuleName.IMPORTING,
|
||||
ModuleName.ADDING,
|
||||
ModuleName.EDITING,
|
||||
ModuleName.ACTIONS,
|
||||
ModuleName.KEYBOARD,
|
||||
],
|
||||
});
|
||||
|
||||
export async function setupImportLogPage(
|
||||
params: LogParams,
|
||||
): Promise<ImportLogPage> {
|
||||
await i18n;
|
||||
|
||||
checkNightMode();
|
||||
|
||||
return new ImportLogPage({
|
||||
target: document.body,
|
||||
props: {
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (window.location.hash.startsWith("#test-")) {
|
||||
const path = window.location.hash.replace("#test-", "");
|
||||
setupImportLogPage({ type: "apkg", path });
|
||||
}
|
|
@ -4,49 +4,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import type { ImportResponse } from "@tslib/anki/import_export_pb";
|
||||
import { importDone, importJsonFile, importJsonString } from "@tslib/backend";
|
||||
import { bridgeCommand } from "@tslib/bridgecommand";
|
||||
import * as tr from "@tslib/ftl";
|
||||
|
||||
import BackendProgressIndicator from "../components/BackendProgressIndicator.svelte";
|
||||
import Container from "../components/Container.svelte";
|
||||
import CloseButton from "./CloseButton.svelte";
|
||||
import DetailsTable from "./DetailsTable.svelte";
|
||||
import { getSummaries } from "./lib";
|
||||
import QueueSummary from "./QueueSummary.svelte";
|
||||
import type { LogParams } from "./types";
|
||||
|
||||
export let params: LogParams;
|
||||
export let response: ImportResponse | undefined = undefined;
|
||||
let result = response;
|
||||
export let response: ImportResponse;
|
||||
const result = response;
|
||||
$: summaries = result ? getSummaries(result.log!) : [];
|
||||
$: foundNotes = result?.log?.foundNotes ?? 0;
|
||||
let closeButton: HTMLElement;
|
||||
|
||||
async function onImport(): Promise<ImportResponse | undefined> {
|
||||
const postOptions = { alertOnError: false };
|
||||
let result: ImportResponse | undefined;
|
||||
try {
|
||||
switch (params.type) {
|
||||
case "json_file":
|
||||
result = await importJsonFile({ val: params.path }, postOptions);
|
||||
break;
|
||||
case "json_string":
|
||||
result = await importJsonString({ val: params.json }, postOptions);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
bridgeCommand("close");
|
||||
throw err;
|
||||
}
|
||||
await importDone({});
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="import-log-page">
|
||||
<BackendProgressIndicator task={onImport} bind:result />
|
||||
{#if result}
|
||||
<p class="note-count">
|
||||
{tr.importingNotesFoundInFile2({
|
67
ts/import-page/ImportPage.svelte
Normal file
67
ts/import-page/ImportPage.svelte
Normal file
|
@ -0,0 +1,67 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
export interface Importer {
|
||||
doImport: () => Promise<ImportResponse>;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ImportResponse } from "@tslib/anki/import_export_pb";
|
||||
import { importDone } from "@tslib/backend";
|
||||
|
||||
import BackendProgressIndicator from "../components/BackendProgressIndicator.svelte";
|
||||
import Container from "../components/Container.svelte";
|
||||
import ErrorPage from "../components/ErrorPage.svelte";
|
||||
import StickyHeader from "../components/StickyHeader.svelte";
|
||||
import ImportLogPage from "./ImportLogPage.svelte";
|
||||
|
||||
export let path: string;
|
||||
export let importer: Importer;
|
||||
export const noOptions: boolean = false;
|
||||
|
||||
let importResponse: ImportResponse | undefined = undefined;
|
||||
let error: Error | undefined = undefined;
|
||||
let importing = noOptions;
|
||||
|
||||
async function onImport(): Promise<ImportResponse> {
|
||||
const result = await importer.doImport();
|
||||
await importDone({});
|
||||
importing = false;
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<ErrorPage {error} />
|
||||
{:else if importResponse}
|
||||
<ImportLogPage response={importResponse} />
|
||||
{:else if importing}
|
||||
<BackendProgressIndicator task={onImport} bind:result={importResponse} bind:error />
|
||||
{:else}
|
||||
<div class="pre-import-page">
|
||||
<StickyHeader {path} onImport={() => (importing = true)} />
|
||||
<Container
|
||||
breakpoint="sm"
|
||||
--gutter-inline="0.25rem"
|
||||
--gutter-block="0.75rem"
|
||||
class="container-columns"
|
||||
>
|
||||
<slot />
|
||||
</Container>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
:global(.row) {
|
||||
// rows have negative margins by default
|
||||
--bs-gutter-x: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pre-import-page {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
54
ts/import-page/index.ts
Normal file
54
ts/import-page/index.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import "./import-page-base.scss";
|
||||
|
||||
import { importJsonFile, importJsonString } from "@tslib/backend";
|
||||
import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||
import { checkNightMode } from "@tslib/nightmode";
|
||||
|
||||
import ImportPage from "./ImportPage.svelte";
|
||||
import type { LogParams } from "./types";
|
||||
|
||||
const i18n = setupI18n({
|
||||
modules: [
|
||||
ModuleName.IMPORTING,
|
||||
ModuleName.ADDING,
|
||||
ModuleName.EDITING,
|
||||
ModuleName.ACTIONS,
|
||||
ModuleName.KEYBOARD,
|
||||
],
|
||||
});
|
||||
|
||||
const postOptions = { alertOnError: false };
|
||||
|
||||
export async function setupImportPage(
|
||||
params: LogParams,
|
||||
): Promise<ImportPage> {
|
||||
await i18n;
|
||||
|
||||
checkNightMode();
|
||||
|
||||
return new ImportPage({
|
||||
target: document.body,
|
||||
props: {
|
||||
path: params.path,
|
||||
noOptions: true,
|
||||
importer: {
|
||||
doImport: () => {
|
||||
switch (params.type) {
|
||||
case "json_file":
|
||||
return importJsonFile({ val: params.path }, postOptions);
|
||||
case "json_string":
|
||||
return importJsonString({ val: params.json }, postOptions);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (window.location.hash.startsWith("#test-")) {
|
||||
const path = window.location.hash.replace("#test-", "");
|
||||
setupImportPage({ type: "json_file", path });
|
||||
}
|
|
@ -23,12 +23,12 @@ export type NoteRow = {
|
|||
};
|
||||
|
||||
type PathParams = {
|
||||
type?: "apkg" | "json_file";
|
||||
type: "json_file";
|
||||
path: string;
|
||||
};
|
||||
|
||||
type JsonParams = {
|
||||
type?: "json_string";
|
||||
type: "json_string";
|
||||
path: string;
|
||||
json: string;
|
||||
};
|
|
@ -38,4 +38,9 @@ export const HelpPage = {
|
|||
updating: "https://docs.ankiweb.net/importing/packaged-decks.html#updating",
|
||||
scheduling: "https://docs.ankiweb.net/importing/packaged-decks.html#scheduling",
|
||||
},
|
||||
TextImporting: {
|
||||
root: "https://docs.ankiweb.net/importing/text-files.html",
|
||||
updating: "https://docs.ankiweb.net/importing/text-files.html#duplicates-and-updating",
|
||||
html: "https://docs.ankiweb.net/importing/text-files.html#html",
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue