mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Add start of new import csv screen
This commit is contained in:
parent
8ad0fdc732
commit
250a1fc495
27 changed files with 729 additions and 334 deletions
|
@ -12,6 +12,7 @@ importing-colon = Colon
|
||||||
importing-column = Column { $val }
|
importing-column = Column { $val }
|
||||||
importing-comma = Comma
|
importing-comma = Comma
|
||||||
importing-empty-first-field = Empty first field: { $val }
|
importing-empty-first-field = Empty first field: { $val }
|
||||||
|
importing-field-delimiter = Field delimiter
|
||||||
importing-field-mapping = Field mapping
|
importing-field-mapping = Field mapping
|
||||||
importing-field-of-file-is = Field <b>{ $val }</b> of file is:
|
importing-field-of-file-is = Field <b>{ $val }</b> of file is:
|
||||||
importing-fields-separated-by = Fields separated by: { $val }
|
importing-fields-separated-by = Fields separated by: { $val }
|
||||||
|
@ -38,6 +39,7 @@ importing-notes-that-could-not-be-imported = Notes that could not be imported as
|
||||||
importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
|
importing-notes-updated-as-file-had-newer = Notes updated, as file had newer version: { $val }
|
||||||
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
|
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
|
||||||
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
||||||
|
importing-pipe = Pipe
|
||||||
importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } fields, expected { $expected }
|
importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } fields, expected { $expected }
|
||||||
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
|
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
|
||||||
importing-semicolon = Semicolon
|
importing-semicolon = Semicolon
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
notetypes-notetype = Notetype
|
||||||
|
|
||||||
## Default field names in newly created note types
|
## Default field names in newly created note types
|
||||||
|
|
||||||
notetypes-front-field = Front
|
notetypes-front-field = Front
|
||||||
|
|
|
@ -27,6 +27,7 @@ service NotetypesService {
|
||||||
rpc GetChangeNotetypeInfo(GetChangeNotetypeInfoRequest)
|
rpc GetChangeNotetypeInfo(GetChangeNotetypeInfoRequest)
|
||||||
returns (ChangeNotetypeInfo);
|
returns (ChangeNotetypeInfo);
|
||||||
rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges);
|
rpc ChangeNotetype(ChangeNotetypeRequest) returns (collection.OpChanges);
|
||||||
|
rpc GetFieldNames(NotetypeId) returns (generic.StringList);
|
||||||
}
|
}
|
||||||
|
|
||||||
message NotetypeId {
|
message NotetypeId {
|
||||||
|
|
|
@ -34,7 +34,7 @@ BrowserRow = search_pb2.BrowserRow
|
||||||
BrowserColumns = search_pb2.BrowserColumns
|
BrowserColumns = search_pb2.BrowserColumns
|
||||||
StripHtmlMode = card_rendering_pb2.StripHtmlRequest
|
StripHtmlMode = card_rendering_pb2.StripHtmlRequest
|
||||||
ImportLogWithChanges = import_export_pb2.ImportResponse
|
ImportLogWithChanges = import_export_pb2.ImportResponse
|
||||||
CsvColumn = import_export_pb2.ImportCsvRequest.CsvColumn
|
ImportCsvRequest = import_export_pb2.ImportCsvRequest
|
||||||
CsvMetadata = import_export_pb2.CsvMetadata
|
CsvMetadata = import_export_pb2.CsvMetadata
|
||||||
Delimiter = import_export_pb2.CsvMetadata.Delimiter
|
Delimiter = import_export_pb2.CsvMetadata.Delimiter
|
||||||
|
|
||||||
|
@ -410,23 +410,9 @@ class Collection(DeprecatedNamesMixin):
|
||||||
request = import_export_pb2.CsvMetadataRequest(path=path, delimiter=delimiter)
|
request = import_export_pb2.CsvMetadataRequest(path=path, delimiter=delimiter)
|
||||||
return self._backend.get_csv_metadata(request)
|
return self._backend.get_csv_metadata(request)
|
||||||
|
|
||||||
def import_csv(
|
def import_csv(self, request: ImportCsvRequest) -> ImportLogWithChanges:
|
||||||
self,
|
log = self._backend.import_csv_raw(request.SerializeToString())
|
||||||
path: str,
|
return ImportLogWithChanges.FromString(log)
|
||||||
deck_id: DeckId,
|
|
||||||
notetype_id: NotetypeId,
|
|
||||||
columns: list[CsvColumn],
|
|
||||||
delimiter: Delimiter.V,
|
|
||||||
is_html: bool,
|
|
||||||
) -> ImportLogWithChanges:
|
|
||||||
return self._backend.import_csv(
|
|
||||||
path=path,
|
|
||||||
deck_id=deck_id,
|
|
||||||
notetype_id=notetype_id,
|
|
||||||
delimiter=delimiter,
|
|
||||||
columns=columns,
|
|
||||||
is_html=is_html,
|
|
||||||
)
|
|
||||||
|
|
||||||
def import_json_file(self, path: str) -> ImportLogWithChanges:
|
def import_json_file(self, path: str) -> ImportLogWithChanges:
|
||||||
return self._backend.import_json_file(path)
|
return self._backend.import_json_file(path)
|
||||||
|
|
|
@ -7,6 +7,7 @@ _pages = [
|
||||||
"change-notetype",
|
"change-notetype",
|
||||||
"card-info",
|
"card-info",
|
||||||
"fields",
|
"fields",
|
||||||
|
"import-csv",
|
||||||
]
|
]
|
||||||
|
|
||||||
[copy_files_into_group(
|
[copy_files_into_group(
|
||||||
|
|
62
qt/aqt/import_export/import_csv_dialog.py
Normal file
62
qt/aqt/import_export/import_csv_dialog.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# 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 aqt
|
||||||
|
import aqt.deckconf
|
||||||
|
import aqt.main
|
||||||
|
import aqt.operations
|
||||||
|
from anki.collection import ImportCsvRequest
|
||||||
|
from aqt.qt import *
|
||||||
|
from aqt.utils import addCloseShortcut, disable_help_button, restoreGeom, saveGeom, tr
|
||||||
|
from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
|
|
||||||
|
class ImportCsvDialog(QDialog):
|
||||||
|
|
||||||
|
TITLE = "csv import"
|
||||||
|
silentlyClose = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mw: aqt.main.AnkiQt,
|
||||||
|
path: str,
|
||||||
|
on_accepted: Callable[[ImportCsvRequest], None],
|
||||||
|
) -> None:
|
||||||
|
QDialog.__init__(self, mw)
|
||||||
|
self.mw = mw
|
||||||
|
self._on_accepted = on_accepted
|
||||||
|
self._setup_ui(path)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def _setup_ui(self, path: str) -> None:
|
||||||
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
self.mw.garbage_collect_on_dialog_finish(self)
|
||||||
|
self.setMinimumSize(400, 300)
|
||||||
|
disable_help_button(self)
|
||||||
|
restoreGeom(self, self.TITLE)
|
||||||
|
addCloseShortcut(self)
|
||||||
|
|
||||||
|
self.web = AnkiWebView(title=self.TITLE)
|
||||||
|
self.web.setVisible(False)
|
||||||
|
self.web.load_ts_page("import-csv")
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.addWidget(self.web)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.web.eval(f"anki.setupImportCsvPage('{path}');")
|
||||||
|
self.setWindowTitle(tr.decks_import_file())
|
||||||
|
|
||||||
|
def reject(self) -> None:
|
||||||
|
self.web.cleanup()
|
||||||
|
self.web = None
|
||||||
|
saveGeom(self, self.TITLE)
|
||||||
|
QDialog.reject(self)
|
||||||
|
|
||||||
|
def do_import(self, data: bytes) -> None:
|
||||||
|
request = ImportCsvRequest()
|
||||||
|
request.ParseFromString(data)
|
||||||
|
self._on_accepted(request)
|
||||||
|
super().reject()
|
|
@ -1,310 +0,0 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
"""Stopgap / not production-ready."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional, Sequence
|
|
||||||
|
|
||||||
import aqt.forms
|
|
||||||
import aqt.main
|
|
||||||
from anki.collection import CsvColumn, CsvMetadata, Delimiter
|
|
||||||
from anki.decks import DeckId
|
|
||||||
from anki.models import NotetypeDict, NotetypeId
|
|
||||||
from aqt.import_export.importing import import_progress_update, show_import_log
|
|
||||||
from aqt.operations import CollectionOp, QueryOp
|
|
||||||
from aqt.qt import *
|
|
||||||
from aqt.utils import HelpPage, disable_help_button, getText, openHelp, showWarning, tr
|
|
||||||
|
|
||||||
DELIMITERS = (
|
|
||||||
("\\t", tr.importing_tab(), Delimiter.TAB),
|
|
||||||
(",", tr.importing_comma(), Delimiter.COMMA),
|
|
||||||
(" ", tr.studying_space(), Delimiter.SPACE),
|
|
||||||
(";", tr.importing_semicolon(), Delimiter.SEMICOLON),
|
|
||||||
(":", tr.importing_colon(), Delimiter.COLON),
|
|
||||||
("|", tr.importing_colon(), Delimiter.PIPE),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeMap(QDialog):
|
|
||||||
def __init__(self, mw: aqt.main.AnkiQt, model: dict, current: str) -> None:
|
|
||||||
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
|
||||||
self.mw = mw
|
|
||||||
self.model = model
|
|
||||||
self.frm = aqt.forms.changemap.Ui_ChangeMap()
|
|
||||||
self.frm.setupUi(self)
|
|
||||||
disable_help_button(self)
|
|
||||||
n = 0
|
|
||||||
setCurrent = False
|
|
||||||
for field in self.model["flds"]:
|
|
||||||
item = QListWidgetItem(tr.importing_map_to(val=field["name"]))
|
|
||||||
self.frm.fields.addItem(item)
|
|
||||||
if current == field["name"]:
|
|
||||||
setCurrent = True
|
|
||||||
self.frm.fields.setCurrentRow(n)
|
|
||||||
n += 1
|
|
||||||
self.frm.fields.addItem(QListWidgetItem(tr.importing_map_to_tags()))
|
|
||||||
self.frm.fields.addItem(QListWidgetItem(tr.importing_ignore_field()))
|
|
||||||
if not setCurrent:
|
|
||||||
if current == "_tags":
|
|
||||||
self.frm.fields.setCurrentRow(n)
|
|
||||||
else:
|
|
||||||
self.frm.fields.setCurrentRow(n + 1)
|
|
||||||
self.field: Optional[str] = None
|
|
||||||
|
|
||||||
def getField(self) -> str:
|
|
||||||
self.exec()
|
|
||||||
return self.field
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
row = self.frm.fields.currentRow()
|
|
||||||
if row < len(self.model["flds"]):
|
|
||||||
self.field = self.model["flds"][row]["name"]
|
|
||||||
elif row == self.frm.fields.count() - 2:
|
|
||||||
self.field = "_tags"
|
|
||||||
else:
|
|
||||||
self.field = None
|
|
||||||
QDialog.accept(self)
|
|
||||||
|
|
||||||
def reject(self) -> None:
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
|
|
||||||
# called by importFile() when importing a mappable file like .csv
|
|
||||||
# ImportType = Union[Importer,AnkiPackageImporter, TextImporter]
|
|
||||||
|
|
||||||
|
|
||||||
class ImportDialog(QDialog):
|
|
||||||
def __init__(self, mw: aqt.main.AnkiQt, path: str) -> None:
|
|
||||||
QDialog.__init__(self, mw, Qt.WindowType.Window)
|
|
||||||
self.mw = mw
|
|
||||||
self.path = path
|
|
||||||
self.options = CsvMetadata()
|
|
||||||
QueryOp(
|
|
||||||
parent=self,
|
|
||||||
op=lambda col: col.get_csv_metadata(path, None),
|
|
||||||
success=self._run,
|
|
||||||
).run_in_background()
|
|
||||||
self._setup_ui()
|
|
||||||
|
|
||||||
def _setup_ui(self) -> None:
|
|
||||||
self.frm = aqt.forms.importing.Ui_ImportDialog()
|
|
||||||
self.frm.setupUi(self)
|
|
||||||
qconnect(
|
|
||||||
self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked,
|
|
||||||
self.helpRequested,
|
|
||||||
)
|
|
||||||
disable_help_button(self)
|
|
||||||
self.setupMappingFrame()
|
|
||||||
qconnect(self.frm.autoDetect.clicked, self.onDelimiter)
|
|
||||||
qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged)
|
|
||||||
# import button
|
|
||||||
b = QPushButton(tr.actions_import())
|
|
||||||
self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole)
|
|
||||||
|
|
||||||
def _run(self, options: CsvMetadata) -> None:
|
|
||||||
self._setup_options(options)
|
|
||||||
self._setup_choosers()
|
|
||||||
self.column_map = ColumnMap(self.columns, self.model)
|
|
||||||
self._render_mapping()
|
|
||||||
self._set_delimiter()
|
|
||||||
self.frm.allowHTML.setChecked(self.is_html)
|
|
||||||
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1))
|
|
||||||
self.frm.tagModified.setText(self.tags)
|
|
||||||
self.frm.tagModified.setCol(self.mw.col)
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def _setup_options(self, options: CsvMetadata) -> None:
|
|
||||||
self.delimiter = options.delimiter
|
|
||||||
self.tags = self.options.tags or self.mw.pm.profile.get("tagModified", "")
|
|
||||||
self.columns = options.columns
|
|
||||||
self.deck_id = DeckId(
|
|
||||||
self.options.deck_id or self.mw.col.get_config("curDeck", default=1)
|
|
||||||
)
|
|
||||||
if options.notetype_id:
|
|
||||||
self.notetype_id = NotetypeId(self.options.notetype_id)
|
|
||||||
self.model = self.mw.col.models.get(self.notetype_id)
|
|
||||||
else:
|
|
||||||
self.model = self.mw.col.models.current()
|
|
||||||
self.notetype_id = self.model["id"]
|
|
||||||
if self.options.is_html is None:
|
|
||||||
self.is_html = self.mw.pm.profile.get("allowHTML", True)
|
|
||||||
else:
|
|
||||||
self.is_html = self.options.is_html
|
|
||||||
|
|
||||||
def _setup_choosers(self) -> None:
|
|
||||||
import aqt.deckchooser
|
|
||||||
import aqt.notetypechooser
|
|
||||||
|
|
||||||
def change_notetype(ntid: NotetypeId) -> None:
|
|
||||||
self.model = self.mw.col.models.get(ntid)
|
|
||||||
self.notetype_id = ntid
|
|
||||||
self.column_map = ColumnMap(self.columns, self.model)
|
|
||||||
self._render_mapping()
|
|
||||||
|
|
||||||
self.modelChooser = aqt.notetypechooser.NotetypeChooser(
|
|
||||||
mw=self.mw,
|
|
||||||
widget=self.frm.modelArea,
|
|
||||||
starting_notetype_id=self.notetype_id,
|
|
||||||
on_notetype_changed=change_notetype,
|
|
||||||
)
|
|
||||||
self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False)
|
|
||||||
|
|
||||||
def _set_delimiter(self) -> None:
|
|
||||||
for delimiter in DELIMITERS:
|
|
||||||
if delimiter[2] == self.delimiter:
|
|
||||||
txt = tr.importing_fields_separated_by(val=delimiter[1])
|
|
||||||
self.frm.autoDetect.setText(txt)
|
|
||||||
return
|
|
||||||
|
|
||||||
def onDelimiter(self) -> None:
|
|
||||||
# Open a modal dialog to enter an delimiter
|
|
||||||
# Todo/Idea Constrain the maximum width, so it doesnt take up that much screen space
|
|
||||||
delim, ok = getText(
|
|
||||||
tr.importing_by_default_anki_will_detect_the(),
|
|
||||||
self,
|
|
||||||
help=HelpPage.IMPORTING,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
# Check if the entered value is valid and if not fallback to default
|
|
||||||
|
|
||||||
txt = ""
|
|
||||||
for delimiter in DELIMITERS:
|
|
||||||
if delimiter[0] == delim:
|
|
||||||
txt = tr.importing_fields_separated_by(val=delimiter[1])
|
|
||||||
self.delimiter = delimiter[2]
|
|
||||||
break
|
|
||||||
if not txt:
|
|
||||||
showWarning(
|
|
||||||
tr.importing_multicharacter_separators_are_not_supported_please()
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.frm.autoDetect.setText(txt)
|
|
||||||
|
|
||||||
def _update_columns(options: CsvMetadata) -> None:
|
|
||||||
self.columns = options.columns
|
|
||||||
self.column_map = ColumnMap(self.columns, self.model)
|
|
||||||
self._render_mapping()
|
|
||||||
|
|
||||||
QueryOp(
|
|
||||||
parent=self,
|
|
||||||
op=lambda col: col.get_csv_metadata(self.path, self.delimiter),
|
|
||||||
success=_update_columns,
|
|
||||||
).run_in_background()
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
# self.mw.pm.profile["importMode"] = self.importer.importMode
|
|
||||||
self.mw.pm.profile["allowHTML"] = self.frm.allowHTML.isChecked()
|
|
||||||
# self.mw.pm.profile["tagModified"] = self.importer.tagModified
|
|
||||||
self.mw.col.set_aux_notetype_config(
|
|
||||||
self.model["id"], "lastDeck", self.deck.selected_deck_id
|
|
||||||
)
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
CollectionOp(
|
|
||||||
parent=self.mw,
|
|
||||||
op=lambda col: col.import_csv(
|
|
||||||
path=self.path,
|
|
||||||
deck_id=self.deck.selected_deck_id,
|
|
||||||
notetype_id=self.model["id"],
|
|
||||||
delimiter=self.delimiter,
|
|
||||||
columns=self.column_map.csv_columns(),
|
|
||||||
is_html=self.frm.allowHTML.isChecked(),
|
|
||||||
),
|
|
||||||
).with_backend_progress(import_progress_update).success(
|
|
||||||
show_import_log
|
|
||||||
).run_in_background()
|
|
||||||
|
|
||||||
def setupMappingFrame(self) -> None:
|
|
||||||
# qt seems to have a bug with adding/removing from a grid, so we add
|
|
||||||
# to a separate object and add/remove that instead
|
|
||||||
self.frame = QFrame(self.frm.mappingArea)
|
|
||||||
self.frm.mappingArea.setWidget(self.frame)
|
|
||||||
self.mapbox = QVBoxLayout(self.frame)
|
|
||||||
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.mapwidget: Optional[QWidget] = None
|
|
||||||
|
|
||||||
def hideMapping(self) -> None:
|
|
||||||
self.frm.mappingGroup.hide()
|
|
||||||
|
|
||||||
def _render_mapping(self) -> None:
|
|
||||||
# set up the mapping grid
|
|
||||||
if self.mapwidget:
|
|
||||||
self.mapbox.removeWidget(self.mapwidget)
|
|
||||||
self.mapwidget.deleteLater()
|
|
||||||
self.mapwidget = QWidget()
|
|
||||||
self.mapbox.addWidget(self.mapwidget)
|
|
||||||
self.grid = QGridLayout(self.mapwidget)
|
|
||||||
self.mapwidget.setLayout(self.grid)
|
|
||||||
self.grid.setContentsMargins(3, 3, 3, 3)
|
|
||||||
self.grid.setSpacing(6)
|
|
||||||
for (num, column) in enumerate(self.column_map.columns):
|
|
||||||
self.grid.addWidget(QLabel(column), num, 0)
|
|
||||||
self.grid.addWidget(QLabel(self.column_map.map_label(num)), num, 1)
|
|
||||||
button = QPushButton(tr.importing_change())
|
|
||||||
self.grid.addWidget(button, num, 2)
|
|
||||||
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
|
|
||||||
|
|
||||||
def changeMappingNum(self, n: int) -> None:
|
|
||||||
f = ChangeMap(self.mw, self.model, self.column_map.map[n]).getField()
|
|
||||||
self.column_map.update(n, f)
|
|
||||||
self._render_mapping()
|
|
||||||
|
|
||||||
def reject(self) -> None:
|
|
||||||
self.modelChooser.cleanup()
|
|
||||||
self.deck.cleanup()
|
|
||||||
QDialog.reject(self)
|
|
||||||
|
|
||||||
def helpRequested(self) -> None:
|
|
||||||
openHelp(HelpPage.IMPORTING)
|
|
||||||
|
|
||||||
def importModeChanged(self, newImportMode: int) -> None:
|
|
||||||
if newImportMode == 0:
|
|
||||||
self.frm.tagModified.setEnabled(True)
|
|
||||||
else:
|
|
||||||
self.frm.tagModified.setEnabled(False)
|
|
||||||
|
|
||||||
|
|
||||||
class ColumnMap:
|
|
||||||
columns: list[str]
|
|
||||||
fields: list[str]
|
|
||||||
map: list[str]
|
|
||||||
|
|
||||||
def __init__(self, columns: Sequence[str], notetype: NotetypeDict) -> None:
|
|
||||||
self.columns = list(columns)
|
|
||||||
self.fields = [f["name"] for f in notetype["flds"]] + ["_tags"]
|
|
||||||
self.map = [""] * len(self.columns)
|
|
||||||
for i in range(min(len(self.fields), len(self.columns))):
|
|
||||||
self.map[i] = self.fields[i]
|
|
||||||
|
|
||||||
def map_label(self, num: int) -> str:
|
|
||||||
name = self.map[num]
|
|
||||||
if not name:
|
|
||||||
return tr.importing_ignored()
|
|
||||||
if name == "_tags":
|
|
||||||
tr.importing_mapped_to_tags()
|
|
||||||
return tr.importing_mapped_to(val=name)
|
|
||||||
|
|
||||||
def update(self, column: int, new_field: str | None) -> None:
|
|
||||||
if new_field:
|
|
||||||
try:
|
|
||||||
idx = self.map.index(new_field)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.map[idx] = ""
|
|
||||||
self.map[column] = new_field or ""
|
|
||||||
|
|
||||||
def csv_columns(self) -> list[CsvColumn]:
|
|
||||||
return [self._column_for_name(name) for name in self.map]
|
|
||||||
|
|
||||||
def _column_for_name(self, name: str) -> CsvColumn:
|
|
||||||
if not name:
|
|
||||||
return CsvColumn(other=CsvColumn.IGNORE)
|
|
||||||
if name == "_tags":
|
|
||||||
return CsvColumn(other=CsvColumn.TAGS)
|
|
||||||
return CsvColumn(field=self.fields.index(name))
|
|
|
@ -8,10 +8,11 @@ from itertools import chain
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
import aqt.main
|
import aqt.main
|
||||||
from anki.collection import Collection, ImportLogWithChanges, Progress
|
from anki.collection import Collection, ImportCsvRequest, ImportLogWithChanges, 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_csv_dialog import ImportCsvDialog
|
||||||
from aqt.operations import CollectionOp, QueryOp
|
from aqt.operations import CollectionOp, QueryOp
|
||||||
from aqt.progress import ProgressUpdate
|
from aqt.progress import ProgressUpdate
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -105,9 +106,15 @@ 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:
|
||||||
import aqt.import_export.import_dialog
|
def on_accepted(request: ImportCsvRequest) -> None:
|
||||||
|
CollectionOp(
|
||||||
|
parent=mw,
|
||||||
|
op=lambda col: col.import_csv(request),
|
||||||
|
).with_backend_progress(import_progress_update).success(
|
||||||
|
show_import_log
|
||||||
|
).run_in_background()
|
||||||
|
|
||||||
aqt.import_export.import_dialog.ImportDialog(mw, path)
|
ImportCsvDialog(mw, path, on_accepted)
|
||||||
|
|
||||||
|
|
||||||
class JsonImporter(Importer):
|
class JsonImporter(Importer):
|
||||||
|
|
|
@ -31,6 +31,7 @@ from anki.scheduler.v3 import NextStates
|
||||||
from anki.utils import dev_mode
|
from anki.utils import dev_mode
|
||||||
from aqt.changenotetype import ChangeNotetypeDialog
|
from aqt.changenotetype import ChangeNotetypeDialog
|
||||||
from aqt.deckoptions import DeckOptionsDialog
|
from aqt.deckoptions import DeckOptionsDialog
|
||||||
|
from aqt.import_export.import_csv_dialog import ImportCsvDialog
|
||||||
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
|
||||||
|
@ -438,6 +439,18 @@ def change_notetype() -> bytes:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def import_csv() -> bytes:
|
||||||
|
data = request.data
|
||||||
|
|
||||||
|
def handle_on_main() -> None:
|
||||||
|
window = aqt.mw.app.activeWindow()
|
||||||
|
if isinstance(window, ImportCsvDialog):
|
||||||
|
window.do_import(data)
|
||||||
|
|
||||||
|
aqt.mw.taskman.run_on_main(handle_on_main)
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
post_handler_list = [
|
post_handler_list = [
|
||||||
congrats_info,
|
congrats_info,
|
||||||
get_deck_configs_for_update,
|
get_deck_configs_for_update,
|
||||||
|
@ -445,13 +458,19 @@ post_handler_list = [
|
||||||
next_card_states,
|
next_card_states,
|
||||||
set_next_card_states,
|
set_next_card_states,
|
||||||
change_notetype,
|
change_notetype,
|
||||||
|
import_csv,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
exposed_backend_list = [
|
exposed_backend_list = [
|
||||||
|
# DeckService
|
||||||
|
"get_deck_names",
|
||||||
# I18nService
|
# I18nService
|
||||||
"i18n_resources",
|
"i18n_resources",
|
||||||
|
# ImportExportService
|
||||||
|
"get_csv_metadata",
|
||||||
# NotesService
|
# NotesService
|
||||||
|
"get_field_names",
|
||||||
"get_note",
|
"get_note",
|
||||||
# NotetypesService
|
# NotetypesService
|
||||||
"get_notetype_names",
|
"get_notetype_names",
|
||||||
|
|
|
@ -168,9 +168,15 @@ impl NotetypesService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_notetype(&self, input: pb::ChangeNotetypeRequest) -> Result<pb::OpChanges> {
|
fn change_notetype(&self, input: pb::ChangeNotetypeRequest) -> Result<pb::OpChanges> {
|
||||||
self.with_col(|col| col.change_notetype_of_notes(input.into()).map(Into::into))
|
self.with_col(|col| col.change_notetype_of_notes(input.into()).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_field_names(&self, input: pb::NotetypeId) -> Result<pb::StringList> {
|
||||||
|
self.with_col(|col| col.storage.get_field_names(input.into()))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<pb::Notetype> for Notetype {
|
impl From<pb::Notetype> for Notetype {
|
||||||
|
|
|
@ -130,9 +130,11 @@ impl Collection {
|
||||||
|
|
||||||
fn column_label(&self, idx: usize, column: &str) -> String {
|
fn column_label(&self, idx: usize, column: &str) -> String {
|
||||||
match column.trim() {
|
match column.trim() {
|
||||||
"" => self.tr.importing_column(idx + 1).to_string(),
|
"" => self.tr.importing_column(idx + 1).into(),
|
||||||
"tags" => self.tr.editing_tags().to_string(),
|
"tags" => self.tr.editing_tags().into(),
|
||||||
s => s.to_string(),
|
"notetype" => self.tr.notetypes_notetype().into(),
|
||||||
|
"deck" => self.tr.decks_deck().into(),
|
||||||
|
s => s.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -374,4 +374,12 @@ impl SqliteStorage {
|
||||||
self.db.execute("update col set models = ?", [json])?;
|
self.db.execute("update col set models = ?", [json])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_field_names(&self, notetype_id: NotetypeId) -> Result<Vec<String>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("SELECT name FROM fields WHERE ntid = ? ORDER BY ord")?
|
||||||
|
.query_and_then([notetype_id], |row| Ok(row.get(0)?))?
|
||||||
|
//.map_err(Into::into)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
87
ts/import-csv/BUILD.bazel
Normal file
87
ts/import-csv/BUILD.bazel
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||||
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
|
load("//ts:generate_page.bzl", "generate_page")
|
||||||
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
load("//ts:typescript.bzl", "typescript")
|
||||||
|
load("//ts:jest.bzl", "jest_test")
|
||||||
|
|
||||||
|
generate_page(page = "import-csv")
|
||||||
|
|
||||||
|
compile_sass(
|
||||||
|
srcs = ["import-csv-base.scss"],
|
||||||
|
group = "base_css",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//sass:base_lib",
|
||||||
|
"//sass:scrollbar_lib",
|
||||||
|
"//sass/bootstrap",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
_ts_deps = [
|
||||||
|
"//ts/components",
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/sveltelib",
|
||||||
|
"@npm//@fluent",
|
||||||
|
"@npm//@types/jest",
|
||||||
|
"@npm//lodash-es",
|
||||||
|
"@npm//svelte",
|
||||||
|
"@npm//marked",
|
||||||
|
]
|
||||||
|
|
||||||
|
compile_svelte(deps = _ts_deps)
|
||||||
|
|
||||||
|
typescript(
|
||||||
|
name = "index",
|
||||||
|
deps = _ts_deps + [
|
||||||
|
":svelte",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
esbuild(
|
||||||
|
name = "import-csv",
|
||||||
|
args = {
|
||||||
|
"globalName": "anki",
|
||||||
|
"loader": {".svg": "text"},
|
||||||
|
},
|
||||||
|
entry_point = "index.ts",
|
||||||
|
output_css = "import-csv.css",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":base_css",
|
||||||
|
":index",
|
||||||
|
":svelte",
|
||||||
|
"@npm//bootstrap-icons",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
################
|
||||||
|
|
||||||
|
prettier_test()
|
||||||
|
|
||||||
|
eslint_test()
|
||||||
|
|
||||||
|
svelte_check(
|
||||||
|
name = "svelte_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]) + [
|
||||||
|
"//sass:button_mixins_lib",
|
||||||
|
"//sass/bootstrap",
|
||||||
|
"@npm//@types/bootstrap",
|
||||||
|
"@npm//@types/lodash-es",
|
||||||
|
"@npm//@types/marked",
|
||||||
|
"//ts/components",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jest_test(
|
||||||
|
protobuf = True,
|
||||||
|
deps = [
|
||||||
|
":index",
|
||||||
|
],
|
||||||
|
)
|
38
ts/import-csv/DeckSelector.svelte
Normal file
38
ts/import-csv/DeckSelector.svelte
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
import SelectButton from "../components/SelectButton.svelte";
|
||||||
|
import SelectOption from "../components/SelectOption.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import type { Decks } from "../lib/proto";
|
||||||
|
|
||||||
|
export let deckNameIds: Decks.DeckNameId[];
|
||||||
|
export let deckId: number;
|
||||||
|
|
||||||
|
function updateCurrentId(event: Event) {
|
||||||
|
const index = parseInt((event.target! as HTMLSelectElement).value);
|
||||||
|
deckId = deckNameIds[index].id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Row --cols={2}>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<div>{tr.decksDeck()}</div>
|
||||||
|
</Col>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<ButtonGroup class="flex-grow-1">
|
||||||
|
<SelectButton class="flex-grow-1" on:change={updateCurrentId}>
|
||||||
|
{#each deckNameIds as entry, idx}
|
||||||
|
<SelectOption value={String(idx)} selected={entry.id === deckId}>
|
||||||
|
{entry.name}
|
||||||
|
</SelectOption>
|
||||||
|
{/each}
|
||||||
|
</SelectButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
54
ts/import-csv/DelimiterSelector.svelte
Normal file
54
ts/import-csv/DelimiterSelector.svelte
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
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
import SelectButton from "../components/SelectButton.svelte";
|
||||||
|
import SelectOption from "../components/SelectOption.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import { ImportExport } from "../lib/proto";
|
||||||
|
|
||||||
|
export let delimiter: ImportExport.CsvMetadata.Delimiter;
|
||||||
|
|
||||||
|
const delimiters = delimiterNames();
|
||||||
|
|
||||||
|
function updateCurrentDelimiter(event: Event) {
|
||||||
|
const index = parseInt((event.target! as HTMLSelectElement).value);
|
||||||
|
delimiter = delimiters[index][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function delimiterNames(): [ImportExport.CsvMetadata.Delimiter, string][] {
|
||||||
|
const Delimiter = ImportExport.CsvMetadata.Delimiter;
|
||||||
|
return [
|
||||||
|
[Delimiter.TAB, tr.importingTab()],
|
||||||
|
[Delimiter.PIPE, tr.importingPipe()],
|
||||||
|
[Delimiter.SEMICOLON, tr.importingSemicolon()],
|
||||||
|
[Delimiter.COLON, tr.importingColon()],
|
||||||
|
[Delimiter.COMMA, tr.importingComma()],
|
||||||
|
[Delimiter.SPACE, tr.studyingSpace()],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Row --cols={2}>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<div>{tr.importingFieldDelimiter()}</div>
|
||||||
|
</Col>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<ButtonGroup class="flex-grow-1">
|
||||||
|
<SelectButton class="flex-grow-1" on:change={updateCurrentDelimiter}>
|
||||||
|
{#each delimiters as delimiterName, idx}
|
||||||
|
<SelectOption
|
||||||
|
value={String(idx)}
|
||||||
|
selected={delimiterName[0] === delimiter}
|
||||||
|
>
|
||||||
|
{delimiterName[1]}
|
||||||
|
</SelectOption>
|
||||||
|
{/each}
|
||||||
|
</SelectButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
43
ts/import-csv/FieldMapper.svelte
Normal file
43
ts/import-csv/FieldMapper.svelte
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Container from "../components/Container.svelte";
|
||||||
|
import Spacer from "../components/Spacer.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import { getNotetypeFields } from "./lib";
|
||||||
|
import MapperRow from "./MapperRow.svelte";
|
||||||
|
|
||||||
|
export let columnNames: string[];
|
||||||
|
export let notetypeId: number;
|
||||||
|
export let fieldColumnIndices: number[];
|
||||||
|
|
||||||
|
$: options = [tr.changeNotetypeNothing()].concat(columnNames);
|
||||||
|
|
||||||
|
let fieldNames: string[] = [];
|
||||||
|
$: {
|
||||||
|
getNotetypeFields(notetypeId).then((newFieldNames) => {
|
||||||
|
fieldNames = newFieldNames;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstFieldIndex: number = 0;
|
||||||
|
$: otherFieldIndices = Array(Math.max(0, fieldNames.length - 1))
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (i + 1 < columnNames.length ? i + 2 : 0));
|
||||||
|
|
||||||
|
$: fieldColumnIndices = [firstFieldIndex, ...otherFieldIndices.map((i) => i - 1)];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Spacer --height="0.5rem" />
|
||||||
|
|
||||||
|
<Container --gutter-inline="0.5rem" --gutter-block="0.15rem">
|
||||||
|
{#each fieldNames as label, idx}
|
||||||
|
{#if idx === 0}
|
||||||
|
<MapperRow {label} options={columnNames} bind:index={firstFieldIndex} />
|
||||||
|
{:else}
|
||||||
|
<MapperRow {label} {options} bind:index={otherFieldIndices[idx - 1]} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</Container>
|
21
ts/import-csv/Header.svelte
Normal file
21
ts/import-csv/Header.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import StickyContainer from "../components/StickyContainer.svelte";
|
||||||
|
|
||||||
|
export let heading: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<StickyContainer --sticky-border="var(--border)" --sticky-borders="0px 0 1px">
|
||||||
|
<h1>
|
||||||
|
{heading}
|
||||||
|
</h1>
|
||||||
|
</StickyContainer>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
33
ts/import-csv/ImportButton.svelte
Normal file
33
ts/import-csv/ImportButton.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
|
import LabelButton from "../components/LabelButton.svelte";
|
||||||
|
import Shortcut from "../components/Shortcut.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import { getPlatformString } from "../lib/shortcuts";
|
||||||
|
|
||||||
|
export let onImport: () => void;
|
||||||
|
|
||||||
|
function doImport(): void {
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
onImport();
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyCombination = "Control+Enter";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<LabelButton
|
||||||
|
theme="primary"
|
||||||
|
tooltip={getPlatformString(keyCombination)}
|
||||||
|
on:click={doImport}
|
||||||
|
--border-left-radius="5px"
|
||||||
|
--border-right-radius="5px">{tr.actionsImport()}</LabelButton
|
||||||
|
>
|
||||||
|
<Shortcut {keyCombination} on:action={doImport} />
|
||||||
|
</ButtonGroup>
|
90
ts/import-csv/ImportCsvPage.svelte
Normal file
90
ts/import-csv/ImportCsvPage.svelte
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import Container from "../components/Container.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
import Switch from "../deck-options/Switch.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import { Decks, ImportExport, importExport, Notetypes } from "../lib/proto";
|
||||||
|
import DeckSelector from "./DeckSelector.svelte";
|
||||||
|
import DelimiterSelector from "./DelimiterSelector.svelte";
|
||||||
|
import FieldMapper from "./FieldMapper.svelte";
|
||||||
|
import Header from "./Header.svelte";
|
||||||
|
import ImportButton from "./ImportButton.svelte";
|
||||||
|
import { getCsvMetadata } from "./lib";
|
||||||
|
import MetaMapper from "./MetaMapper.svelte";
|
||||||
|
import NotetypeSelector from "./NotetypeSelector.svelte";
|
||||||
|
|
||||||
|
export let path: string;
|
||||||
|
export let notetypeNameIds: Notetypes.NotetypeNameId[];
|
||||||
|
export let deckNameIds: Decks.DeckNameId[];
|
||||||
|
export let delimiter: ImportExport.CsvMetadata.Delimiter;
|
||||||
|
// TODO
|
||||||
|
export const tags: string = "";
|
||||||
|
export let columnNames: string[];
|
||||||
|
export let notetypeId: number;
|
||||||
|
export let deckId: number;
|
||||||
|
export let isHtml: boolean;
|
||||||
|
|
||||||
|
let fieldColumnIndices: number[];
|
||||||
|
let tagsColumn: number;
|
||||||
|
let deckColumn: number;
|
||||||
|
let notetypeColumn: number;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
getCsvMetadata(path, delimiter).then((meta) => {
|
||||||
|
columnNames = meta.columns;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onImport(): Promise<void> {
|
||||||
|
await importExport.importCsv(
|
||||||
|
ImportExport.ImportCsvRequest.create({
|
||||||
|
path,
|
||||||
|
deckId,
|
||||||
|
notetypeId,
|
||||||
|
delimiter,
|
||||||
|
isHtml,
|
||||||
|
columns: ImportExport.ImportCsvRequest.Columns.create({
|
||||||
|
fields: fieldColumnIndices,
|
||||||
|
tags: tagsColumn,
|
||||||
|
deck: deckColumn,
|
||||||
|
notetype: notetypeColumn,
|
||||||
|
}),
|
||||||
|
columnNames,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="--gutter-inline: 0.25rem;">
|
||||||
|
<Row class="gx-0" --cols={2}>
|
||||||
|
<Col --col-size={1} breakpoint="md">
|
||||||
|
<Container>
|
||||||
|
<Header heading={tr.importingImportOptions()} />
|
||||||
|
<NotetypeSelector {notetypeNameIds} bind:notetypeId />
|
||||||
|
<DeckSelector {deckNameIds} bind:deckId />
|
||||||
|
<DelimiterSelector bind:delimiter />
|
||||||
|
<Switch id={undefined} bind:value={isHtml}>
|
||||||
|
{tr.importingAllowHtmlInFields()}
|
||||||
|
</Switch>
|
||||||
|
</Container>
|
||||||
|
</Col>
|
||||||
|
<Col --col-size={1} breakpoint="md">
|
||||||
|
<Container>
|
||||||
|
<Header heading={tr.importingFieldMapping()} />
|
||||||
|
<FieldMapper {columnNames} {notetypeId} bind:fieldColumnIndices />
|
||||||
|
<MetaMapper
|
||||||
|
{columnNames}
|
||||||
|
bind:tagsColumn
|
||||||
|
bind:notetypeColumn
|
||||||
|
bind:deckColumn
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row><ImportButton {onImport} /></Row>
|
||||||
|
</div>
|
29
ts/import-csv/MapperRow.svelte
Normal file
29
ts/import-csv/MapperRow.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let options: string[];
|
||||||
|
export let index: number = 0;
|
||||||
|
|
||||||
|
const labelIndex = options.indexOf(label);
|
||||||
|
index = labelIndex > 0 ? labelIndex : index;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Row --cols={2}>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
{label}
|
||||||
|
</Col>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<!-- svelte-ignore a11y-no-onchange -->
|
||||||
|
<select class="form-select" bind:value={index}>
|
||||||
|
{#each options as name, idx}
|
||||||
|
<option value={idx}>{name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
25
ts/import-csv/MetaMapper.svelte
Normal file
25
ts/import-csv/MetaMapper.svelte
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Container from "../components/Container.svelte";
|
||||||
|
import Spacer from "../components/Spacer.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import MapperRow from "./MapperRow.svelte";
|
||||||
|
|
||||||
|
export let columnNames: string[];
|
||||||
|
export let tagsColumn: number = 0;
|
||||||
|
export let deckColumn: number = 0;
|
||||||
|
export let notetypeColumn: number = 0;
|
||||||
|
|
||||||
|
$: options = [tr.changeNotetypeNothing()].concat(columnNames);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Spacer --height="0.5rem" />
|
||||||
|
|
||||||
|
<Container --gutter-inline="0.5rem" --gutter-block="0.15rem">
|
||||||
|
<MapperRow label={tr.editingTags()} {options} bind:index={tagsColumn} />
|
||||||
|
<MapperRow label={tr.decksDeck()} {options} bind:index={deckColumn} />
|
||||||
|
<MapperRow label={tr.notetypesNotetype()} {options} bind:index={notetypeColumn} />
|
||||||
|
</Container>
|
41
ts/import-csv/NotetypeSelector.svelte
Normal file
41
ts/import-csv/NotetypeSelector.svelte
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
|
import Col from "../components/Col.svelte";
|
||||||
|
import Row from "../components/Row.svelte";
|
||||||
|
import SelectButton from "../components/SelectButton.svelte";
|
||||||
|
import SelectOption from "../components/SelectOption.svelte";
|
||||||
|
import * as tr from "../lib/ftl";
|
||||||
|
import type { Notetypes } from "../lib/proto";
|
||||||
|
|
||||||
|
export let notetypeNameIds: Notetypes.NotetypeNameId[];
|
||||||
|
export let notetypeId: number;
|
||||||
|
|
||||||
|
function updateCurrentId(event: Event) {
|
||||||
|
const index = parseInt((event.target! as HTMLSelectElement).value);
|
||||||
|
notetypeId = notetypeNameIds[index].id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Row --cols={2}>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<div>{tr.notetypesNotetype()}</div>
|
||||||
|
</Col>
|
||||||
|
<Col --col-size={1}>
|
||||||
|
<ButtonGroup class="flex-grow-1">
|
||||||
|
<SelectButton class="flex-grow-1" on:change={updateCurrentId}>
|
||||||
|
{#each notetypeNameIds as entry, idx}
|
||||||
|
<SelectOption
|
||||||
|
value={String(idx)}
|
||||||
|
selected={entry.id === notetypeId}
|
||||||
|
>
|
||||||
|
{entry.name}
|
||||||
|
</SelectOption>
|
||||||
|
{/each}
|
||||||
|
</SelectButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
34
ts/import-csv/import-csv-base.scss
Normal file
34
ts/import-csv/import-csv-base.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@use "sass/vars";
|
||||||
|
@use "sass/bootstrap-dark";
|
||||||
|
|
||||||
|
@import "sass/base";
|
||||||
|
|
||||||
|
@import "sass/bootstrap/scss/alert";
|
||||||
|
@import "sass/bootstrap/scss/buttons";
|
||||||
|
@import "sass/bootstrap/scss/button-group";
|
||||||
|
@import "sass/bootstrap/scss/close";
|
||||||
|
@import "sass/bootstrap/scss/grid";
|
||||||
|
@import "sass/bootstrap-forms";
|
||||||
|
|
||||||
|
.night-mode {
|
||||||
|
@include bootstrap-dark.night-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: min(100vw, 70em);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
padding: 0.5em 0.5em 1em 0.5em;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// override the default down arrow colour in <select> elements
|
||||||
|
.night-mode select {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||||
|
}
|
67
ts/import-csv/index.ts
Normal file
67
ts/import-csv/index.ts
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
|
||||||
|
|
||||||
|
import "./import-csv-base.css";
|
||||||
|
|
||||||
|
import { ModuleName, setupI18n } from "../lib/i18n";
|
||||||
|
import { checkNightMode } from "../lib/nightmode";
|
||||||
|
import {
|
||||||
|
Decks,
|
||||||
|
decks as decksService,
|
||||||
|
empty,
|
||||||
|
notetypes as notetypeService,
|
||||||
|
} from "../lib/proto";
|
||||||
|
import ImportCsvPage from "./ImportCsvPage.svelte";
|
||||||
|
import { getCsvMetadata } from "./lib";
|
||||||
|
|
||||||
|
const gettingNotetypes = notetypeService.getNotetypeNames(empty);
|
||||||
|
const gettingDecks = decksService.getDeckNames(
|
||||||
|
Decks.GetDeckNamesRequest.create({
|
||||||
|
skipEmptyDefault: false,
|
||||||
|
includeFiltered: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const i18n = setupI18n({
|
||||||
|
modules: [
|
||||||
|
ModuleName.ACTIONS,
|
||||||
|
ModuleName.CHANGE_NOTETYPE,
|
||||||
|
ModuleName.DECKS,
|
||||||
|
ModuleName.EDITING,
|
||||||
|
ModuleName.IMPORTING,
|
||||||
|
ModuleName.NOTETYPES,
|
||||||
|
ModuleName.STUDYING,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setupImportCsvPage(path: string): Promise<ImportCsvPage> {
|
||||||
|
const gettingMetadata = getCsvMetadata(path);
|
||||||
|
const [notetypes, decks, metadata] = await Promise.all([
|
||||||
|
gettingNotetypes,
|
||||||
|
gettingDecks,
|
||||||
|
gettingMetadata,
|
||||||
|
i18n,
|
||||||
|
]);
|
||||||
|
|
||||||
|
checkNightMode();
|
||||||
|
|
||||||
|
return new ImportCsvPage({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
path: path,
|
||||||
|
notetypeNameIds: notetypes.entries,
|
||||||
|
deckNameIds: decks.entries,
|
||||||
|
delimiter: metadata.delimiter,
|
||||||
|
columnNames: metadata.columns,
|
||||||
|
tags: metadata.tags,
|
||||||
|
notetypeId: metadata.notetypeId,
|
||||||
|
deckId: metadata.deckId,
|
||||||
|
isHtml: metadata.isHtml!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // use #testXXXX where XXXX is notetype ID to test
|
||||||
|
if (window.location.hash.startsWith("#test")) {
|
||||||
|
const ntid = parseInt(window.location.hash.substr("#test".length), 10);
|
||||||
|
setupCsvImportPage(ntid, ntid);
|
||||||
|
} */
|
27
ts/import-csv/lib.ts
Normal file
27
ts/import-csv/lib.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import {
|
||||||
|
ImportExport,
|
||||||
|
importExport,
|
||||||
|
Notetypes,
|
||||||
|
notetypes as notetypeService,
|
||||||
|
} from "../lib/proto";
|
||||||
|
|
||||||
|
export async function getNotetypeFields(notetypeId: number): Promise<string[]> {
|
||||||
|
return notetypeService
|
||||||
|
.getFieldNames(Notetypes.NotetypeId.create({ ntid: notetypeId }))
|
||||||
|
.then((list) => list.vals);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCsvMetadata(
|
||||||
|
path: string,
|
||||||
|
delimiter?: ImportExport.CsvMetadata.Delimiter,
|
||||||
|
): Promise<ImportExport.CsvMetadata> {
|
||||||
|
return importExport.getCsvMetadata(
|
||||||
|
ImportExport.CsvMetadataRequest.create({
|
||||||
|
path,
|
||||||
|
delimiter,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
12
ts/import-csv/tsconfig.json
Normal file
12
ts/import-csv/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["*"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../lib" },
|
||||||
|
{ "path": "../sveltelib" },
|
||||||
|
{ "path": "../components" }
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["jest"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import DeckConfig = anki.deckconfig;
|
||||||
import Decks = anki.decks;
|
import Decks = anki.decks;
|
||||||
import Generic = anki.generic;
|
import Generic = anki.generic;
|
||||||
import I18n = anki.i18n;
|
import I18n = anki.i18n;
|
||||||
|
import ImportExport = anki.import_export;
|
||||||
import Notes = anki.notes;
|
import Notes = anki.notes;
|
||||||
import Notetypes = anki.notetypes;
|
import Notetypes = anki.notetypes;
|
||||||
import Scheduler = anki.scheduler;
|
import Scheduler = anki.scheduler;
|
||||||
|
@ -54,6 +55,8 @@ async function serviceCallback(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const decks = Decks.DecksService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
export { DeckConfig };
|
export { DeckConfig };
|
||||||
export const deckConfig = DeckConfig.DeckConfigService.create(
|
export const deckConfig = DeckConfig.DeckConfigService.create(
|
||||||
serviceCallback as RPCImpl,
|
serviceCallback as RPCImpl,
|
||||||
|
@ -62,6 +65,11 @@ export const deckConfig = DeckConfig.DeckConfigService.create(
|
||||||
export { I18n };
|
export { I18n };
|
||||||
export const i18n = I18n.I18nService.create(serviceCallback as RPCImpl);
|
export const i18n = I18n.I18nService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
export { ImportExport };
|
||||||
|
export const importExport = ImportExport.ImportExportService.create(
|
||||||
|
serviceCallback as RPCImpl,
|
||||||
|
);
|
||||||
|
|
||||||
export { Notetypes };
|
export { Notetypes };
|
||||||
export const notetypes = Notetypes.NotetypesService.create(serviceCallback as RPCImpl);
|
export const notetypes = Notetypes.NotetypesService.create(serviceCallback as RPCImpl);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue