mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12: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(
|
build_page(
|
||||||
"import-log",
|
"import-page",
|
||||||
true,
|
true,
|
||||||
inputs![
|
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-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-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-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.
|
## 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
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.deckconf
|
import aqt.deckconf
|
||||||
|
@ -14,45 +15,76 @@ from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGe
|
||||||
from aqt.webview import AnkiWebView, AnkiWebViewKind
|
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):
|
class ImportDialog(QDialog):
|
||||||
TITLE: str
|
|
||||||
KIND: AnkiWebViewKind
|
|
||||||
TS_PAGE: str
|
|
||||||
SETUP_FUNCTION_NAME: str
|
|
||||||
DEFAULT_SIZE = (800, 800)
|
DEFAULT_SIZE = (800, 800)
|
||||||
MIN_SIZE = (400, 300)
|
MIN_SIZE = (400, 300)
|
||||||
silentlyClose = True
|
silentlyClose = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, mw: aqt.main.AnkiQt, args: ImportArgs) -> None:
|
||||||
self,
|
|
||||||
mw: aqt.main.AnkiQt,
|
|
||||||
path: str,
|
|
||||||
) -> None:
|
|
||||||
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._setup_ui(path)
|
self.args = args
|
||||||
|
self._setup_ui()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _setup_ui(self, path: str) -> None:
|
def _setup_ui(self) -> None:
|
||||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
self.mw.garbage_collect_on_dialog_finish(self)
|
self.mw.garbage_collect_on_dialog_finish(self)
|
||||||
self.setMinimumSize(*self.MIN_SIZE)
|
self.setMinimumSize(*self.MIN_SIZE)
|
||||||
disable_help_button(self)
|
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)
|
addCloseShortcut(self)
|
||||||
|
|
||||||
self.web = AnkiWebView(kind=self.KIND)
|
self.web = AnkiWebView(kind=self.args.kind)
|
||||||
self.web.setVisible(False)
|
self.web.setVisible(False)
|
||||||
self.web.load_ts_page(self.TS_PAGE)
|
self.web.load_ts_page(self.args.ts_page)
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.addWidget(self.web)
|
layout.addWidget(self.web)
|
||||||
self.setLayout(layout)
|
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(
|
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(),
|
lambda _: self.web.setFocus(),
|
||||||
)
|
)
|
||||||
self.setWindowTitle(tr.decks_import_file())
|
self.setWindowTitle(tr.decks_import_file())
|
||||||
|
@ -62,19 +94,5 @@ class ImportDialog(QDialog):
|
||||||
self.mw.col.set_wants_abort()
|
self.mw.col.set_wants_abort()
|
||||||
self.web.cleanup()
|
self.web.cleanup()
|
||||||
self.web = None
|
self.web = None
|
||||||
saveGeom(self, self.TITLE)
|
saveGeom(self, self.args.title)
|
||||||
QDialog.reject(self)
|
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.errors import Interrupted
|
||||||
from anki.foreign_data import mnemosyne
|
from anki.foreign_data import mnemosyne
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from aqt.import_export.import_dialog import ImportAnkiPackageDialog, ImportCsvDialog
|
from aqt.import_export.import_dialog import (
|
||||||
from aqt.import_export.import_log_dialog import (
|
AnkiPackageArgs,
|
||||||
ImportLogDialog,
|
CsvArgs,
|
||||||
|
ImportDialog,
|
||||||
JsonFileArgs,
|
JsonFileArgs,
|
||||||
JsonStringArgs,
|
JsonStringArgs,
|
||||||
)
|
)
|
||||||
|
@ -87,7 +88,7 @@ class ApkgImporter(Importer):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||||
ImportAnkiPackageDialog(mw, path)
|
ImportDialog(mw, AnkiPackageArgs(path))
|
||||||
|
|
||||||
|
|
||||||
class MnemosyneImporter(Importer):
|
class MnemosyneImporter(Importer):
|
||||||
|
@ -98,9 +99,7 @@ class MnemosyneImporter(Importer):
|
||||||
QueryOp(
|
QueryOp(
|
||||||
parent=mw,
|
parent=mw,
|
||||||
op=lambda col: mnemosyne.serialize(path, col.decks.current()["id"]),
|
op=lambda col: mnemosyne.serialize(path, col.decks.current()["id"]),
|
||||||
success=lambda json: ImportLogDialog(
|
success=lambda json: ImportDialog(mw, JsonStringArgs(path=path, json=json)),
|
||||||
mw, JsonStringArgs(path=path, json=json)
|
|
||||||
),
|
|
||||||
).with_progress().run_in_background()
|
).with_progress().run_in_background()
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,7 +108,7 @@ class CsvImporter(Importer):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||||
ImportCsvDialog(mw, path)
|
ImportDialog(mw, CsvArgs(path))
|
||||||
|
|
||||||
|
|
||||||
class JsonImporter(Importer):
|
class JsonImporter(Importer):
|
||||||
|
@ -117,7 +116,7 @@ class JsonImporter(Importer):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
def do_import(mw: aqt.main.AnkiQt, path: str) -> None:
|
||||||
ImportLogDialog(mw, JsonFileArgs(path=path))
|
ImportDialog(mw, JsonFileArgs(path=path))
|
||||||
|
|
||||||
|
|
||||||
IMPORTERS: list[Type[Importer]] = [
|
IMPORTERS: list[Type[Importer]] = [
|
||||||
|
|
|
@ -425,9 +425,8 @@ def import_done() -> bytes:
|
||||||
def update_window_modality() -> None:
|
def update_window_modality() -> None:
|
||||||
if window := aqt.mw.app.activeWindow():
|
if window := aqt.mw.app.activeWindow():
|
||||||
from aqt.import_export.import_dialog import ImportDialog
|
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.hide()
|
||||||
window.setWindowModality(Qt.WindowModality.NonModal)
|
window.setWindowModality(Qt.WindowModality.NonModal)
|
||||||
window.show()
|
window.show()
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl FileOp {
|
||||||
impl FileIoError {
|
impl FileIoError {
|
||||||
pub fn message(&self) -> String {
|
pub fn message(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Failed to {} '{}':<br>{}",
|
"Failed to {} '{}': {}",
|
||||||
match &self.op {
|
match &self.op {
|
||||||
FileOp::Unknown => return format!("{}", self.source),
|
FileOp::Unknown => return format!("{}", self.source),
|
||||||
FileOp::Open => "open".into(),
|
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 };
|
type ResultWithChanges = OpChanges | { changes?: OpChanges };
|
||||||
|
|
||||||
export let task: () => Promise<ResultWithChanges | undefined>;
|
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 = "";
|
let label: string = "";
|
||||||
|
|
||||||
function onUpdate(progress: Progress) {
|
function onUpdate(progress: Progress) {
|
||||||
|
@ -24,8 +25,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: (async () => {
|
$: (async () => {
|
||||||
if (!result) {
|
if (!result && !error) {
|
||||||
result = await runWithBackendProgress(task, onUpdate);
|
try {
|
||||||
|
result = await runWithBackendProgress(task, onUpdate);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error = err;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,21 +2,34 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
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">
|
<script lang="ts">
|
||||||
import Select from "./Select.svelte";
|
import Select from "./Select.svelte";
|
||||||
import SelectOption from "./SelectOption.svelte";
|
import SelectOption from "./SelectOption.svelte";
|
||||||
|
|
||||||
export let options: string[] = [];
|
type T = $$Generic;
|
||||||
export let disabled: number[] = [];
|
|
||||||
export let value = 0;
|
|
||||||
|
|
||||||
$: 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>
|
</script>
|
||||||
|
|
||||||
<Select bind:value {label}>
|
<Select bind:value {label} {disabled}>
|
||||||
{#each options as option, idx}
|
{#each choices as { label: optionLabel, value: optionValue }}
|
||||||
<SelectOption value={idx} disabled={disabled.includes(idx)}>
|
<SelectOption
|
||||||
{option}
|
value={optionValue}
|
||||||
|
disabled={disabledChoices.includes(optionValue)}
|
||||||
|
>
|
||||||
|
{optionLabel}
|
||||||
</SelectOption>
|
</SelectOption>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
|
@ -5,16 +5,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Col from "./Col.svelte";
|
import Col from "./Col.svelte";
|
||||||
import ConfigInput from "./ConfigInput.svelte";
|
import ConfigInput from "./ConfigInput.svelte";
|
||||||
import EnumSelector from "./EnumSelector.svelte";
|
import EnumSelector, { type Choice } from "./EnumSelector.svelte";
|
||||||
import RevertButton from "./RevertButton.svelte";
|
import RevertButton from "./RevertButton.svelte";
|
||||||
import Row from "./Row.svelte";
|
import Row from "./Row.svelte";
|
||||||
import type { Breakpoint } from "./types";
|
import type { Breakpoint } from "./types";
|
||||||
|
|
||||||
export let value: number;
|
type T = $$Generic;
|
||||||
export let defaultValue: number;
|
|
||||||
|
export let value: T;
|
||||||
|
export let defaultValue: T;
|
||||||
export let breakpoint: Breakpoint = "md";
|
export let breakpoint: Breakpoint = "md";
|
||||||
export let choices: string[];
|
export let choices: Choice<T>[];
|
||||||
export let disabled: number[] = [];
|
export let disabled: boolean = false;
|
||||||
|
export let disabledChoices: T[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row --cols={13}>
|
<Row --cols={13}>
|
||||||
|
@ -23,7 +26,7 @@
|
||||||
</Col>
|
</Col>
|
||||||
<Col --col-size={6} {breakpoint}>
|
<Col --col-size={6} {breakpoint}>
|
||||||
<ConfigInput>
|
<ConfigInput>
|
||||||
<EnumSelector bind:value options={choices} {disabled} />
|
<EnumSelector bind:value {choices} {disabled} {disabledChoices} />
|
||||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||||
</ConfigInput>
|
</ConfigInput>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
export let value: boolean;
|
export let value: boolean;
|
||||||
export let defaultValue: boolean;
|
export let defaultValue: boolean;
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
|
||||||
const id = Math.random().toString(36).substring(2);
|
const id = Math.random().toString(36).substring(2);
|
||||||
</script>
|
</script>
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
<Col --col-size={4}><Label for={id} preventMouseClick><slot /></Label></Col>
|
<Col --col-size={4}><Label for={id} preventMouseClick><slot /></Label></Col>
|
||||||
<Col --col-justify="flex-end">
|
<Col --col-justify="flex-end">
|
||||||
<ConfigInput grow={false}>
|
<ConfigInput grow={false}>
|
||||||
<Switch {id} bind:value />
|
<Switch {id} bind:value {disabled} />
|
||||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||||
</ConfigInput>
|
</ConfigInput>
|
||||||
</Col>
|
</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 SettingTitle from "../components/SettingTitle.svelte";
|
||||||
import TitledContainer from "../components/TitledContainer.svelte";
|
import TitledContainer from "../components/TitledContainer.svelte";
|
||||||
import type { HelpItem } from "../components/types";
|
import type { HelpItem } from "../components/types";
|
||||||
|
import {
|
||||||
|
newGatherPriorityChoices,
|
||||||
|
newSortOrderChoices,
|
||||||
|
reviewMixChoices,
|
||||||
|
reviewOrderChoices,
|
||||||
|
} from "./choices";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import { reviewMixChoices } from "./strings";
|
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
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 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[] = [];
|
let disabledNewSortOrders: number[] = [];
|
||||||
$: {
|
$: {
|
||||||
switch ($config.newCardGatherPriority) {
|
switch ($config.newCardGatherPriority) {
|
||||||
|
@ -143,7 +122,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.newCardGatherPriority}
|
bind:value={$config.newCardGatherPriority}
|
||||||
defaultValue={defaults.newCardGatherPriority}
|
defaultValue={defaults.newCardGatherPriority}
|
||||||
choices={newGatherPriorityChoices}
|
choices={newGatherPriorityChoices()}
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<SettingTitle
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
|
@ -160,8 +139,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.newCardSortOrder}
|
bind:value={$config.newCardSortOrder}
|
||||||
defaultValue={defaults.newCardSortOrder}
|
defaultValue={defaults.newCardSortOrder}
|
||||||
choices={newSortOrderChoices}
|
choices={newSortOrderChoices()}
|
||||||
disabled={disabledNewSortOrders}
|
disabledChoices={disabledNewSortOrders}
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<SettingTitle
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
|
@ -212,7 +191,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.reviewOrder}
|
bind:value={$config.reviewOrder}
|
||||||
defaultValue={defaults.reviewOrder}
|
defaultValue={defaults.reviewOrder}
|
||||||
choices={reviewOrderChoices}
|
choices={reviewOrderChoices()}
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<SettingTitle
|
||||||
on:click={() =>
|
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 SettingTitle from "../components/SettingTitle.svelte";
|
||||||
import TitledContainer from "../components/TitledContainer.svelte";
|
import TitledContainer from "../components/TitledContainer.svelte";
|
||||||
import type { HelpItem } from "../components/types";
|
import type { HelpItem } from "../components/types";
|
||||||
|
import { leechChoices } from "./choices";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
import StepsInputRow from "./StepsInputRow.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 = {
|
const settings = {
|
||||||
relearningSteps: {
|
relearningSteps: {
|
||||||
title: tr.deckConfigRelearningSteps(),
|
title: tr.deckConfigRelearningSteps(),
|
||||||
|
@ -136,7 +135,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.leechAction}
|
bind:value={$config.leechAction}
|
||||||
defaultValue={defaults.leechAction}
|
defaultValue={defaults.leechAction}
|
||||||
choices={leechChoices}
|
choices={leechChoices()}
|
||||||
breakpoint="md"
|
breakpoint="md"
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<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 SettingTitle from "../components/SettingTitle.svelte";
|
||||||
import TitledContainer from "../components/TitledContainer.svelte";
|
import TitledContainer from "../components/TitledContainer.svelte";
|
||||||
import type { HelpItem } from "../components/types";
|
import type { HelpItem } from "../components/types";
|
||||||
|
import { newInsertOrderChoices } from "./choices";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
import StepsInputRow from "./StepsInputRow.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 config = state.currentConfig;
|
||||||
const defaults = state.defaults;
|
const defaults = state.defaults;
|
||||||
|
|
||||||
const newInsertOrderChoices = [
|
|
||||||
tr.deckConfigNewInsertionOrderSequential(),
|
|
||||||
tr.deckConfigNewInsertionOrderRandom(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let stepsExceedGraduatingInterval: string;
|
let stepsExceedGraduatingInterval: string;
|
||||||
$: {
|
$: {
|
||||||
const lastLearnStepInDays = $config.learnSteps.length
|
const lastLearnStepInDays = $config.learnSteps.length
|
||||||
|
@ -155,7 +151,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.newCardInsertOrder}
|
bind:value={$config.newCardInsertOrder}
|
||||||
defaultValue={defaults.newCardInsertOrder}
|
defaultValue={defaults.newCardInsertOrder}
|
||||||
choices={newInsertOrderChoices}
|
choices={newInsertOrderChoices()}
|
||||||
breakpoint={"md"}
|
breakpoint={"md"}
|
||||||
>
|
>
|
||||||
<SettingTitle
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {
|
import type { ImportAnkiPackageOptions } from "@tslib/anki/import_export_pb";
|
||||||
ImportAnkiPackageOptions,
|
|
||||||
ImportResponse,
|
|
||||||
} from "@tslib/anki/import_export_pb";
|
|
||||||
import { importAnkiPackage } from "@tslib/backend";
|
import { importAnkiPackage } from "@tslib/backend";
|
||||||
import { importDone } from "@tslib/backend";
|
|
||||||
import * as tr from "@tslib/ftl";
|
import * as tr from "@tslib/ftl";
|
||||||
import { HelpPage } from "@tslib/help-page";
|
import { HelpPage } from "@tslib/help-page";
|
||||||
import type Carousel from "bootstrap/js/dist/carousel";
|
import type Carousel from "bootstrap/js/dist/carousel";
|
||||||
import type Modal from "bootstrap/js/dist/modal";
|
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 HelpModal from "../components/HelpModal.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
import SettingTitle from "../components/SettingTitle.svelte";
|
import SettingTitle from "../components/SettingTitle.svelte";
|
||||||
import StickyHeader from "../components/StickyHeader.svelte";
|
|
||||||
import SwitchRow from "../components/SwitchRow.svelte";
|
import SwitchRow from "../components/SwitchRow.svelte";
|
||||||
import TitledContainer from "../components/TitledContainer.svelte";
|
import TitledContainer from "../components/TitledContainer.svelte";
|
||||||
import type { HelpItem } from "../components/types";
|
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 path: string;
|
||||||
export let options: ImportAnkiPackageOptions;
|
export let options: ImportAnkiPackageOptions;
|
||||||
|
|
||||||
let importResponse: ImportResponse | undefined = undefined;
|
|
||||||
let importing = false;
|
|
||||||
|
|
||||||
const updateChoices = [
|
|
||||||
tr.importingUpdateIfNewer(),
|
|
||||||
tr.importingUpdateAlways(),
|
|
||||||
tr.importingUpdateNever(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
mergeNotetypes: {
|
mergeNotetypes: {
|
||||||
title: tr.importingMergeNotetypes(),
|
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 modal: Modal;
|
||||||
let carousel: Carousel;
|
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 {
|
function openHelpModal(index: number): void {
|
||||||
modal.show();
|
modal.show();
|
||||||
carousel.to(index);
|
carousel.to(index);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if importing}
|
<ImportPage
|
||||||
<BackendProgressIndicator task={onImport} bind:result={importResponse} />
|
{path}
|
||||||
{:else if importResponse}
|
importer={{
|
||||||
<ImportLogPage response={importResponse} params={{ path }} />
|
doImport: () =>
|
||||||
{:else}
|
importAnkiPackage({ packagePath: path, options }, { alertOnError: false }),
|
||||||
<StickyHeader {path} onImport={() => (importing = true)} />
|
}}
|
||||||
|
>
|
||||||
|
<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
|
<SwitchRow bind:value={options.mergeNotetypes} defaultValue={false}>
|
||||||
breakpoint="sm"
|
<SettingTitle
|
||||||
--gutter-inline="0.25rem"
|
on:click={() =>
|
||||||
--gutter-block="0.75rem"
|
openHelpModal(Object.keys(settings).indexOf("mergeNotetypes"))}
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
<SettingTitle
|
{settings.mergeNotetypes.title}
|
||||||
on:click={() =>
|
</SettingTitle>
|
||||||
openHelpModal(Object.keys(settings).indexOf("updateNotes"))}
|
</SwitchRow>
|
||||||
>
|
|
||||||
{settings.updateNotes.title}
|
|
||||||
</SettingTitle>
|
|
||||||
</EnumSelectorRow>
|
|
||||||
|
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={options.updateNotetypes}
|
bind:value={options.updateNotes}
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
choices={updateChoices}
|
choices={updateChoices()}
|
||||||
|
>
|
||||||
|
<SettingTitle
|
||||||
|
on:click={() =>
|
||||||
|
openHelpModal(Object.keys(settings).indexOf("updateNotes"))}
|
||||||
>
|
>
|
||||||
<SettingTitle
|
{settings.updateNotes.title}
|
||||||
on:click={() =>
|
</SettingTitle>
|
||||||
openHelpModal(
|
</EnumSelectorRow>
|
||||||
Object.keys(settings).indexOf("updateNotetypes"),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{settings.updateNotetypes.title}
|
|
||||||
</SettingTitle>
|
|
||||||
</EnumSelectorRow>
|
|
||||||
|
|
||||||
<SwitchRow bind:value={options.withScheduling} defaultValue={false}>
|
<EnumSelectorRow
|
||||||
<SettingTitle
|
bind:value={options.updateNotetypes}
|
||||||
on:click={() =>
|
defaultValue={0}
|
||||||
openHelpModal(
|
choices={updateChoices()}
|
||||||
Object.keys(settings).indexOf("withScheduling"),
|
>
|
||||||
)}
|
<SettingTitle
|
||||||
>
|
on:click={() =>
|
||||||
{settings.withScheduling.title}
|
openHelpModal(Object.keys(settings).indexOf("updateNotetypes"))}
|
||||||
</SettingTitle>
|
>
|
||||||
</SwitchRow>
|
{settings.updateNotetypes.title}
|
||||||
</TitledContainer>
|
</SettingTitle>
|
||||||
</Row>
|
</EnumSelectorRow>
|
||||||
</Container>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
<SwitchRow bind:value={options.withScheduling} defaultValue={false}>
|
||||||
:global(.row) {
|
<SettingTitle
|
||||||
// rows have negative margins by default
|
on:click={() =>
|
||||||
--bs-gutter-x: 0;
|
openHelpModal(Object.keys(settings).indexOf("withScheduling"))}
|
||||||
margin-bottom: 0.5rem;
|
>
|
||||||
}
|
{settings.withScheduling.title}
|
||||||
</style>
|
</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 {
|
body {
|
||||||
min-height: 100vh;
|
|
||||||
width: min(100vw, 70em);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1em 1em 1em;
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 * as tr from "@tslib/ftl";
|
||||||
|
import TitledContainer from "components/TitledContainer.svelte";
|
||||||
|
|
||||||
import Spacer from "../components/Spacer.svelte";
|
import type { ImportCsvState } from "./lib";
|
||||||
import type { ColumnOption } from "./lib";
|
|
||||||
import MapperRow from "./MapperRow.svelte";
|
import MapperRow from "./MapperRow.svelte";
|
||||||
|
|
||||||
export let columnOptions: ColumnOption[];
|
export let state: ImportCsvState;
|
||||||
export let tagsColumn: number;
|
|
||||||
export let globalNotetype: CsvMetadata_MappedNotetype | null;
|
|
||||||
|
|
||||||
let lastNotetypeId: bigint | undefined = -1n;
|
const metadata = state.metadata;
|
||||||
let fieldNamesPromise: Promise<string[]>;
|
const globalNotetype = state.globalNotetype;
|
||||||
|
const fieldNamesPromise = state.fieldNames;
|
||||||
$: if (globalNotetype?.id !== lastNotetypeId) {
|
const columnOptions = state.columnOptions;
|
||||||
lastNotetypeId = globalNotetype?.id;
|
|
||||||
fieldNamesPromise =
|
|
||||||
globalNotetype === null
|
|
||||||
? Promise.resolve([])
|
|
||||||
: getFieldNames({ ntid: globalNotetype.id }).then((list) => list.vals);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if globalNotetype}
|
<TitledContainer title={tr.importingFieldMapping()}>
|
||||||
{#await fieldNamesPromise then fieldNames}
|
{#if $globalNotetype !== null}
|
||||||
{#each fieldNames as label, idx}
|
{#await $fieldNamesPromise then fieldNames}
|
||||||
<!-- first index is treated specially, because it must be assigned some column -->
|
{#each fieldNames as label, idx}
|
||||||
<MapperRow
|
<!-- first index is treated specially, because it must be assigned some column -->
|
||||||
{label}
|
<MapperRow
|
||||||
columnOptions={idx === 0 ? columnOptions.slice(1) : columnOptions}
|
{label}
|
||||||
bind:value={globalNotetype.fieldColumns[idx]}
|
columnOptions={idx === 0 ? $columnOptions.slice(1) : $columnOptions}
|
||||||
/>
|
bind:value={$globalNotetype.fieldColumns[idx]}
|
||||||
{/each}
|
/>
|
||||||
{/await}
|
{/each}
|
||||||
<Spacer --height="1.5rem" />
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
<MapperRow label={tr.editingTags()} {columnOptions} bind:value={tagsColumn} />
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 Row from "../components/Row.svelte";
|
||||||
import Spacer from "../components/Spacer.svelte";
|
import ImportPage from "../import-page/ImportPage.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 FieldMapper from "./FieldMapper.svelte";
|
import FieldMapper from "./FieldMapper.svelte";
|
||||||
import Header from "./Header.svelte";
|
import FileOptions from "./FileOptions.svelte";
|
||||||
import HtmlSwitch from "./HtmlSwitch.svelte";
|
import ImportOptions from "./ImportOptions.svelte";
|
||||||
import {
|
import type { ImportCsvState } from "./lib";
|
||||||
buildDeckOneof,
|
|
||||||
buildNotetypeOneof,
|
|
||||||
getColumnOptions,
|
|
||||||
tryGetDeckId,
|
|
||||||
tryGetGlobalNotetype,
|
|
||||||
} from "./lib";
|
|
||||||
import NotetypeSelector from "./NotetypeSelector.svelte";
|
|
||||||
import Preview from "./Preview.svelte";
|
|
||||||
import Tags from "./Tags.svelte";
|
|
||||||
|
|
||||||
export let path: string;
|
export let state: ImportCsvState;
|
||||||
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;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="outer">
|
<ImportPage path={state.path} importer={state}>
|
||||||
{#if importing}
|
<Row><FileOptions {state} /></Row>
|
||||||
<BackendProgressIndicator task={onImport} bind:result={importResponse} />
|
<Row><ImportOptions {state} /></Row>
|
||||||
{:else if importResponse}
|
<Row><FieldMapper {state} /></Row>
|
||||||
<ImportLogPage response={importResponse} params={{ path }} />
|
</ImportPage>
|
||||||
{: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>
|
|
||||||
|
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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[];
|
const metadata = state.metadata;
|
||||||
export let preview: StringList[];
|
const columnOptions = state.columnOptions;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="outer">
|
<div class="outer">
|
||||||
<table class="preview">
|
<table class="preview">
|
||||||
{#each columnOptions.slice(1) as { label, shortLabel }}
|
{#each $columnOptions.slice(1) as { label, shortLabel }}
|
||||||
<th>
|
<th>
|
||||||
{shortLabel || label}
|
{shortLabel || label}
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
{#each preview as row}
|
{#each $metadata.preview as row}
|
||||||
<tr>
|
<tr>
|
||||||
{#each row.vals as cell}
|
{#each row.vals as cell}
|
||||||
<td>{cell}</td>
|
<td>{cell}</td>
|
||||||
|
@ -30,8 +30,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.outer {
|
.outer {
|
||||||
// approximate size based on body max width + margins
|
|
||||||
width: min(90vw, 65em);
|
|
||||||
overflow: auto;
|
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/button-group";
|
||||||
@import "bootstrap/scss/close";
|
@import "bootstrap/scss/close";
|
||||||
@import "bootstrap/scss/grid";
|
@import "bootstrap/scss/grid";
|
||||||
|
@import "bootstrap/scss/transitions";
|
||||||
|
@import "bootstrap/scss/modal";
|
||||||
|
@import "bootstrap/scss/carousel";
|
||||||
@import "sass/bootstrap-forms";
|
@import "sass/bootstrap-forms";
|
||||||
@import "sass/bootstrap-tooltip";
|
@import "sass/bootstrap-tooltip";
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { getCsvMetadata, getDeckNames, getNotetypeNames } from "@tslib/backend";
|
||||||
import { ModuleName, setupI18n } from "@tslib/i18n";
|
import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||||
import { checkNightMode } from "@tslib/nightmode";
|
import { checkNightMode } from "@tslib/nightmode";
|
||||||
|
|
||||||
|
import { modalsKey } from "../components/context-keys";
|
||||||
import ErrorPage from "../components/ErrorPage.svelte";
|
import ErrorPage from "../components/ErrorPage.svelte";
|
||||||
import ImportCsvPage from "./ImportCsvPage.svelte";
|
import ImportCsvPage from "./ImportCsvPage.svelte";
|
||||||
import { tryGetDeckColumn, tryGetDeckId, tryGetGlobalNotetype, tryGetNotetypeColumn } from "./lib";
|
import { ImportCsvState } from "./lib";
|
||||||
|
|
||||||
const i18n = setupI18n({
|
const i18n = setupI18n({
|
||||||
modules: [
|
modules: [
|
||||||
|
@ -22,11 +23,16 @@ const i18n = setupI18n({
|
||||||
ModuleName.NOTETYPES,
|
ModuleName.NOTETYPES,
|
||||||
ModuleName.STUDYING,
|
ModuleName.STUDYING,
|
||||||
ModuleName.ADDING,
|
ModuleName.ADDING,
|
||||||
|
ModuleName.HELP,
|
||||||
|
ModuleName.DECK_CONFIG,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function setupImportCsvPage(path: string): Promise<ImportCsvPage | ErrorPage> {
|
export async function setupImportCsvPage(path: string): Promise<ImportCsvPage | ErrorPage> {
|
||||||
|
const context = new Map();
|
||||||
|
context.set(modalsKey, new Map());
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
getNotetypeNames({}),
|
getNotetypeNames({}),
|
||||||
getDeckNames({
|
getDeckNames({
|
||||||
|
@ -35,32 +41,13 @@ export async function setupImportCsvPage(path: string): Promise<ImportCsvPage |
|
||||||
}),
|
}),
|
||||||
getCsvMetadata({ path }, { alertOnError: false }),
|
getCsvMetadata({ path }, { alertOnError: false }),
|
||||||
i18n,
|
i18n,
|
||||||
]).then(([notetypes, decks, metadata, _i18n]) => {
|
]).then(([notetypes, decks, metadata]) => {
|
||||||
return new ImportCsvPage({
|
return new ImportCsvPage({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
path: path,
|
state: new ImportCsvState(path, notetypes, decks, metadata),
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
|
context,
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
return new ErrorPage({ target: document.body, props: { error } });
|
return new ErrorPage({ target: document.body, props: { error } });
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// 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 * 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 {
|
export interface ColumnOption {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -11,24 +18,164 @@ export interface ColumnOption {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColumnOptions(
|
export function getGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype | null {
|
||||||
columnLabels: string[],
|
return meta.notetype.case === "globalNotetype" ? meta.notetype.value : null;
|
||||||
firstRow: string[],
|
}
|
||||||
notetypeColumn: number | null,
|
|
||||||
deckColumn: number | null,
|
export function getDeckId(meta: CsvMetadata): bigint | null {
|
||||||
guidColumn: number,
|
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[] {
|
): ColumnOption[] {
|
||||||
|
const notetypeColumn = getNotetypeColumn(metadata);
|
||||||
|
const deckColumn = getDeckColumn(metadata);
|
||||||
return [{ label: tr.changeNotetypeNothing(), value: 0, disabled: false }].concat(
|
return [{ label: tr.changeNotetypeNothing(), value: 0, disabled: false }].concat(
|
||||||
columnLabels.map((label, index) => {
|
metadata.columnLabels.map((label, index) => {
|
||||||
index += 1;
|
index += 1;
|
||||||
if (index === notetypeColumn) {
|
if (index === notetypeColumn) {
|
||||||
return columnOption(tr.notetypesNotetype(), true, index);
|
return columnOption(tr.notetypesNotetype(), true, index);
|
||||||
} else if (index === deckColumn) {
|
} else if (index === deckColumn) {
|
||||||
return columnOption(tr.decksDeck(), true, index);
|
return columnOption(tr.decksDeck(), true, index);
|
||||||
} else if (index === guidColumn) {
|
} else if (index === metadata.guidColumn) {
|
||||||
return columnOption("GUID", true, index);
|
return columnOption("GUID", true, index);
|
||||||
} else if (label === "") {
|
} else if (label === "") {
|
||||||
return columnOption(firstRow[index - 1], false, index, true);
|
return columnOption(metadata.preview[0].vals[index - 1], false, index, true);
|
||||||
} else {
|
} else {
|
||||||
return columnOption(label, false, index);
|
return columnOption(label, false, index);
|
||||||
}
|
}
|
||||||
|
@ -50,42 +197,10 @@ function columnOption(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryGetGlobalNotetype(meta: CsvMetadata): CsvMetadata_MappedNotetype | null {
|
function getDeckColumn(meta: CsvMetadata): number | 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 {
|
|
||||||
return meta.deck.case === "deckColumn" ? meta.deck.value : 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;
|
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">
|
<script lang="ts">
|
||||||
import type { ImportResponse } from "@tslib/anki/import_export_pb";
|
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 * as tr from "@tslib/ftl";
|
||||||
|
|
||||||
import BackendProgressIndicator from "../components/BackendProgressIndicator.svelte";
|
|
||||||
import Container from "../components/Container.svelte";
|
import Container from "../components/Container.svelte";
|
||||||
import CloseButton from "./CloseButton.svelte";
|
import CloseButton from "./CloseButton.svelte";
|
||||||
import DetailsTable from "./DetailsTable.svelte";
|
import DetailsTable from "./DetailsTable.svelte";
|
||||||
import { getSummaries } from "./lib";
|
import { getSummaries } from "./lib";
|
||||||
import QueueSummary from "./QueueSummary.svelte";
|
import QueueSummary from "./QueueSummary.svelte";
|
||||||
import type { LogParams } from "./types";
|
|
||||||
|
|
||||||
export let params: LogParams;
|
export let response: ImportResponse;
|
||||||
export let response: ImportResponse | undefined = undefined;
|
const result = response;
|
||||||
let result = response;
|
|
||||||
$: summaries = result ? getSummaries(result.log!) : [];
|
$: summaries = result ? getSummaries(result.log!) : [];
|
||||||
$: foundNotes = result?.log?.foundNotes ?? 0;
|
$: foundNotes = result?.log?.foundNotes ?? 0;
|
||||||
let closeButton: HTMLElement;
|
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>
|
</script>
|
||||||
|
|
||||||
<Container class="import-log-page">
|
<Container class="import-log-page">
|
||||||
<BackendProgressIndicator task={onImport} bind:result />
|
|
||||||
{#if result}
|
{#if result}
|
||||||
<p class="note-count">
|
<p class="note-count">
|
||||||
{tr.importingNotesFoundInFile2({
|
{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 PathParams = {
|
||||||
type?: "apkg" | "json_file";
|
type: "json_file";
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type JsonParams = {
|
type JsonParams = {
|
||||||
type?: "json_string";
|
type: "json_string";
|
||||||
path: string;
|
path: string;
|
||||||
json: string;
|
json: string;
|
||||||
};
|
};
|
|
@ -38,4 +38,9 @@ export const HelpPage = {
|
||||||
updating: "https://docs.ankiweb.net/importing/packaged-decks.html#updating",
|
updating: "https://docs.ankiweb.net/importing/packaged-decks.html#updating",
|
||||||
scheduling: "https://docs.ankiweb.net/importing/packaged-decks.html#scheduling",
|
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