commit
3f3c509bad
@ -7,8 +7,8 @@ browsing-all-fields = All Fields
|
||||
browsing-answer = Answer
|
||||
browsing-any-cards-mapped-to-nothing-will = Any cards mapped to nothing will be deleted. If a note has no remaining cards, it will be lost. Are you sure you want to continue?
|
||||
browsing-any-flag = Any Flag
|
||||
browsing-average-ease = Average Ease
|
||||
browsing-average-interval = Average Interval
|
||||
browsing-average-ease = Avg. Ease
|
||||
browsing-average-interval = Avg. Interval
|
||||
browsing-browser-appearance = Browser Appearance
|
||||
browsing-browser-options = Browser Options
|
||||
browsing-buried = Buried
|
||||
@ -101,7 +101,7 @@ browsing-suspended = Suspended
|
||||
browsing-tag-duplicates = Tag Duplicates
|
||||
browsing-tag-rename-warning-empty = You can't rename a tag that has no notes.
|
||||
browsing-target-field = Target field:
|
||||
browsing-toggle-cards-notes-mode = Toggle Cards/Notes Mode
|
||||
browsing-toggle-showing-cards-notes = Toggle Showing Cards/Notes
|
||||
browsing-toggle-mark = Toggle Mark
|
||||
browsing-toggle-suspend = Toggle Suspend
|
||||
browsing-treat-input-as-regular-expression = Treat input as regular expression
|
||||
|
@ -4,6 +4,7 @@ persistent = no
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=
|
||||
BrowserColumns,
|
||||
BrowserRow,
|
||||
FormatTimespanIn,
|
||||
AnswerCardIn,
|
||||
|
@ -12,7 +12,6 @@ SearchNode = _pb.SearchNode
|
||||
Progress = _pb.Progress
|
||||
EmptyCardsReport = _pb.EmptyCardsReport
|
||||
GraphPreferences = _pb.GraphPreferences
|
||||
BuiltinSort = _pb.SortOrder.Builtin
|
||||
Preferences = _pb.Preferences
|
||||
UndoStatus = _pb.UndoStatus
|
||||
OpChanges = _pb.OpChanges
|
||||
@ -21,6 +20,7 @@ OpChangesWithId = _pb.OpChangesWithId
|
||||
OpChangesAfterUndo = _pb.OpChangesAfterUndo
|
||||
DefaultsForAdding = _pb.DeckAndNotetype
|
||||
BrowserRow = _pb.BrowserRow
|
||||
BrowserColumns = _pb.BrowserColumns
|
||||
|
||||
import copy
|
||||
import os
|
||||
@ -40,7 +40,7 @@ from anki.config import Config, ConfigManager
|
||||
from anki.consts import *
|
||||
from anki.dbproxy import DBProxy
|
||||
from anki.decks import Deck, DeckConfig, DeckConfigId, DeckId, DeckManager
|
||||
from anki.errors import AbortSchemaModification, DBError
|
||||
from anki.errors import AbortSchemaModification, DBError, InvalidInput
|
||||
from anki.lang import FormatTimeSpan
|
||||
from anki.media import MediaManager, media_paths_from_col_path
|
||||
from anki.models import ModelManager, Notetype, NotetypeDict, NotetypeId
|
||||
@ -505,7 +505,7 @@ class Collection:
|
||||
def find_cards(
|
||||
self,
|
||||
query: str,
|
||||
order: Union[bool, str, BuiltinSort.Kind.V] = False,
|
||||
order: Union[bool, str, BrowserColumns.Column] = False,
|
||||
reverse: bool = False,
|
||||
) -> Sequence[CardId]:
|
||||
"""Return card ids matching the provided search.
|
||||
@ -520,13 +520,15 @@ class Collection:
|
||||
desc and vice versa when reverse is set in the collection config, eg
|
||||
order="c.ivl asc, c.due desc".
|
||||
|
||||
If order is a BuiltinSort.Kind value, sort using that builtin sort, eg
|
||||
col.find_cards("", order=BuiltinSort.Kind.CARD_DUE)
|
||||
If order is a BrowserColumns.Column that supports sorting, sort using that
|
||||
column. All available columns are available through col.all_browser_columns()
|
||||
or browser.table._model.columns and support sorting unless column.sorting
|
||||
is set to BrowserColumns.SORTING_NONE.
|
||||
|
||||
The reverse argument only applies when a BuiltinSort.Kind is provided;
|
||||
The reverse argument only applies when a BrowserColumns.Column is provided;
|
||||
otherwise the collection config defines whether reverse is set or not.
|
||||
"""
|
||||
mode = _build_sort_mode(order, reverse)
|
||||
mode = self._build_sort_mode(order, reverse, False)
|
||||
return cast(
|
||||
Sequence[CardId], self._backend.search_cards(search=query, order=mode)
|
||||
)
|
||||
@ -534,7 +536,7 @@ class Collection:
|
||||
def find_notes(
|
||||
self,
|
||||
query: str,
|
||||
order: Union[bool, str, BuiltinSort.Kind.V] = False,
|
||||
order: Union[bool, str, BrowserColumns.Column] = False,
|
||||
reverse: bool = False,
|
||||
) -> Sequence[NoteId]:
|
||||
"""Return note ids matching the provided search.
|
||||
@ -542,11 +544,38 @@ class Collection:
|
||||
To programmatically construct a search string, see .build_search_string().
|
||||
The order parameter is documented in .find_cards().
|
||||
"""
|
||||
mode = _build_sort_mode(order, reverse)
|
||||
mode = self._build_sort_mode(order, reverse, True)
|
||||
return cast(
|
||||
Sequence[NoteId], self._backend.search_notes(search=query, order=mode)
|
||||
)
|
||||
|
||||
def _build_sort_mode(
|
||||
self,
|
||||
order: Union[bool, str, BrowserColumns.Column],
|
||||
reverse: bool,
|
||||
finding_notes: bool,
|
||||
) -> _pb.SortOrder:
|
||||
if isinstance(order, str):
|
||||
return _pb.SortOrder(custom=order)
|
||||
if isinstance(order, bool):
|
||||
if order is False:
|
||||
return _pb.SortOrder(none=_pb.Empty())
|
||||
# order=True: set args to sort column and reverse from config
|
||||
sort_key = "noteSortType" if finding_notes else "sortType"
|
||||
order = self.get_browser_column(self.get_config(sort_key))
|
||||
reverse_key = (
|
||||
Config.Bool.BROWSER_NOTE_SORT_BACKWARDS
|
||||
if finding_notes
|
||||
else Config.Bool.BROWSER_SORT_BACKWARDS
|
||||
)
|
||||
reverse = self.get_config_bool(reverse_key)
|
||||
if isinstance(order, BrowserColumns.Column):
|
||||
if order.sorting != BrowserColumns.SORTING_NONE:
|
||||
return _pb.SortOrder(
|
||||
builtin=_pb.SortOrder.Builtin(column=order.key, reverse=reverse)
|
||||
)
|
||||
raise InvalidInput(f"{order} is not a valid sort order.")
|
||||
|
||||
def find_and_replace(
|
||||
self,
|
||||
*,
|
||||
@ -696,6 +725,15 @@ class Collection:
|
||||
# Browser Table
|
||||
##########################################################################
|
||||
|
||||
def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
|
||||
return self._backend.all_browser_columns()
|
||||
|
||||
def get_browser_column(self, key: str) -> Optional[BrowserColumns.Column]:
|
||||
for column in self._backend.all_browser_columns():
|
||||
if column.key == key:
|
||||
return column
|
||||
return None
|
||||
|
||||
def browser_row_for_id(
|
||||
self, id_: int
|
||||
) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
|
||||
@ -712,24 +750,24 @@ class Collection:
|
||||
columns = self.get_config(
|
||||
"activeCols", ["noteFld", "template", "cardDue", "deck"]
|
||||
)
|
||||
self._backend.set_desktop_browser_card_columns(columns)
|
||||
self._backend.set_active_browser_columns(columns)
|
||||
return columns
|
||||
|
||||
def set_browser_card_columns(self, columns: List[str]) -> None:
|
||||
self.set_config("activeCols", columns)
|
||||
self._backend.set_desktop_browser_card_columns(columns)
|
||||
self._backend.set_active_browser_columns(columns)
|
||||
|
||||
def load_browser_note_columns(self) -> List[str]:
|
||||
"""Return the stored note column names and ensure the backend columns are set and in sync."""
|
||||
columns = self.get_config(
|
||||
"activeNoteCols", ["noteFld", "note", "noteCards", "noteTags"]
|
||||
)
|
||||
self._backend.set_desktop_browser_note_columns(columns)
|
||||
self._backend.set_active_browser_columns(columns)
|
||||
return columns
|
||||
|
||||
def set_browser_note_columns(self, columns: List[str]) -> None:
|
||||
self.set_config("activeNoteCols", columns)
|
||||
self._backend.set_desktop_browser_note_columns(columns)
|
||||
self._backend.set_active_browser_columns(columns)
|
||||
|
||||
# Config
|
||||
##########################################################################
|
||||
@ -1111,18 +1149,3 @@ class _ReviewsUndo:
|
||||
|
||||
|
||||
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
|
||||
|
||||
|
||||
def _build_sort_mode(
|
||||
order: Union[bool, str, BuiltinSort.Kind.V],
|
||||
reverse: bool,
|
||||
) -> _pb.SortOrder:
|
||||
if isinstance(order, str):
|
||||
return _pb.SortOrder(custom=order)
|
||||
elif isinstance(order, bool):
|
||||
if order is True:
|
||||
return _pb.SortOrder(from_config=_pb.Empty())
|
||||
else:
|
||||
return _pb.SortOrder(none=_pb.Empty())
|
||||
else:
|
||||
return _pb.SortOrder(builtin=_pb.SortOrder.Builtin(kind=order, reverse=reverse))
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
import pytest
|
||||
|
||||
from anki.collection import BuiltinSort, Config
|
||||
from anki.collection import Config
|
||||
from anki.consts import *
|
||||
from tests.shared import getEmptyCol, isNearCutoff
|
||||
|
||||
@ -125,10 +125,12 @@ def test_findCards():
|
||||
col.flush()
|
||||
assert col.findCards("", order=True)[0] in latestCardIds
|
||||
assert (
|
||||
col.find_cards("", order=BuiltinSort.CARD_DUE, reverse=False)[0] == firstCardId
|
||||
col.find_cards("", order=col.get_browser_column("cardDue"), reverse=False)[0]
|
||||
== firstCardId
|
||||
)
|
||||
assert (
|
||||
col.find_cards("", order=BuiltinSort.CARD_DUE, reverse=True)[0] != firstCardId
|
||||
col.find_cards("", order=col.get_browser_column("cardDue"), reverse=True)[0]
|
||||
!= firstCardId
|
||||
)
|
||||
# model
|
||||
assert len(col.findCards("note:basic")) == 3
|
||||
|
@ -6,6 +6,7 @@ ignore = forms,hooks_gen.py
|
||||
[TYPECHECK]
|
||||
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
|
||||
ignored-classes=
|
||||
BrowserColumns,
|
||||
BrowserRow,
|
||||
SearchNode,
|
||||
Config,
|
||||
|
@ -64,7 +64,6 @@ from aqt.utils import (
|
||||
save_combo_history,
|
||||
save_combo_index_for_session,
|
||||
saveGeom,
|
||||
saveHeader,
|
||||
saveSplitter,
|
||||
saveState,
|
||||
shortcut,
|
||||
@ -228,10 +227,10 @@ class Browser(QMainWindow):
|
||||
def _closeWindow(self) -> None:
|
||||
self._cleanup_preview()
|
||||
self.editor.cleanup()
|
||||
self.table.cleanup()
|
||||
saveSplitter(self.form.splitter, "editor3")
|
||||
saveGeom(self, "editor")
|
||||
saveState(self, "editor")
|
||||
saveHeader(self.form.tableView.horizontalHeader(), "editor")
|
||||
self.teardownHooks()
|
||||
self.mw.maybeReset()
|
||||
aqt.dialogs.markClosed("Browser")
|
||||
@ -383,7 +382,7 @@ class Browser(QMainWindow):
|
||||
self.table.set_view(self.form.tableView)
|
||||
switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial())
|
||||
switch.setChecked(self.table.is_notes_mode())
|
||||
switch.setToolTip(tr.browsing_toggle_cards_notes_mode())
|
||||
switch.setToolTip(tr.browsing_toggle_showing_cards_notes())
|
||||
qconnect(self.form.action_toggle_mode.triggered, switch.toggle)
|
||||
qconnect(switch.toggled, self.on_table_state_changed)
|
||||
self.form.gridLayout.addWidget(switch, 0, 0)
|
||||
|
@ -607,10 +607,10 @@
|
||||
</action>
|
||||
<action name="action_toggle_mode">
|
||||
<property name="text">
|
||||
<string>browsing_toggle_cards_notes_mode</string>
|
||||
<string>browsing_toggle_showing_cards_notes</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string notr="true">Ctrl+M</string>
|
||||
<string notr="true">Alt+T</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
|
227
qt/aqt/table.py
227
qt/aqt/table.py
@ -6,7 +6,6 @@ from __future__ import annotations
|
||||
import time
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from dataclasses import dataclass
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@ -23,6 +22,7 @@ from typing import (
|
||||
import aqt
|
||||
import aqt.forms
|
||||
from anki.cards import Card, CardId
|
||||
from anki.collection import BrowserColumns as Columns
|
||||
from anki.collection import BrowserRow, Collection, Config, OpChanges
|
||||
from anki.consts import *
|
||||
from anki.errors import NotFoundError
|
||||
@ -35,10 +35,12 @@ from aqt.utils import (
|
||||
KeyboardModifiersPressed,
|
||||
qtMenuShortcutWorkaround,
|
||||
restoreHeader,
|
||||
saveHeader,
|
||||
showInfo,
|
||||
tr,
|
||||
)
|
||||
|
||||
Column = Columns.Column
|
||||
ItemId = Union[CardId, NoteId]
|
||||
ItemList = Union[Sequence[CardId], Sequence[NoteId]]
|
||||
|
||||
@ -47,7 +49,8 @@ ItemList = Union[Sequence[CardId], Sequence[NoteId]]
|
||||
class SearchContext:
|
||||
search: str
|
||||
browser: aqt.browser.Browser
|
||||
order: Union[bool, str] = True
|
||||
order: Union[bool, str, Column] = True
|
||||
reverse: bool = False
|
||||
# if set, provided ids will be used instead of the regular search
|
||||
ids: Optional[Sequence[ItemId]] = None
|
||||
|
||||
@ -73,6 +76,9 @@ class Table:
|
||||
self._setup_view()
|
||||
self._setup_headers()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self._save_header()
|
||||
|
||||
# Public Methods
|
||||
######################################################################
|
||||
|
||||
@ -148,11 +154,8 @@ class Table:
|
||||
def select_single_card(self, card_id: CardId) -> None:
|
||||
"""Try to set the selection to the item corresponding to the given card."""
|
||||
self.clear_selection()
|
||||
if self.is_notes_mode():
|
||||
self._view.selectRow(0)
|
||||
else:
|
||||
if (row := self._model.get_card_row(card_id)) is not None:
|
||||
self._view.selectRow(row)
|
||||
if (row := self._model.get_card_row(card_id)) is not None:
|
||||
self._view.selectRow(row)
|
||||
|
||||
# Reset
|
||||
|
||||
@ -198,6 +201,7 @@ class Table:
|
||||
def toggle_state(self, is_notes_mode: bool, last_search: str) -> None:
|
||||
if is_notes_mode == self.is_notes_mode():
|
||||
return
|
||||
self._save_header()
|
||||
self._save_selection()
|
||||
self._state = self._model.toggle_state(
|
||||
SearchContext(search=last_search, browser=self.browser)
|
||||
@ -205,8 +209,7 @@ class Table:
|
||||
self.col.set_config_bool(
|
||||
Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, self.is_notes_mode()
|
||||
)
|
||||
self._set_sort_indicator()
|
||||
self._set_column_sizes()
|
||||
self._restore_header()
|
||||
self._restore_selection(self._toggled_selection)
|
||||
|
||||
# Move cursor
|
||||
@ -273,6 +276,12 @@ class Table:
|
||||
# this must be set post-resize or it doesn't work
|
||||
hh.setCascadingSectionResizes(False)
|
||||
|
||||
def _save_header(self) -> None:
|
||||
saveHeader(self._view.horizontalHeader(), self._state.config_key_prefix)
|
||||
|
||||
def _restore_header(self) -> None:
|
||||
restoreHeader(self._view.horizontalHeader(), self._state.config_key_prefix)
|
||||
|
||||
# Setup
|
||||
|
||||
def _setup_view(self) -> None:
|
||||
@ -314,14 +323,14 @@ class Table:
|
||||
if not isWin:
|
||||
vh.hide()
|
||||
hh.show()
|
||||
restoreHeader(hh, "editor")
|
||||
hh.setHighlightSections(False)
|
||||
hh.setMinimumSectionSize(50)
|
||||
hh.setSectionsMovable(True)
|
||||
self._set_column_sizes()
|
||||
hh.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
qconnect(hh.customContextMenuRequested, self._on_header_context)
|
||||
self._restore_header()
|
||||
self._set_column_sizes()
|
||||
self._set_sort_indicator()
|
||||
qconnect(hh.customContextMenuRequested, self._on_header_context)
|
||||
qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed)
|
||||
qconnect(hh.sectionMoved, self._on_column_moved)
|
||||
|
||||
@ -350,13 +359,13 @@ class Table:
|
||||
def _on_header_context(self, pos: QPoint) -> None:
|
||||
gpos = self._view.mapToGlobal(pos)
|
||||
m = QMenu()
|
||||
for column, name in self._state.columns:
|
||||
a = m.addAction(name)
|
||||
for key, column in self._model.columns.items():
|
||||
a = m.addAction(self._state.column_label(column))
|
||||
a.setCheckable(True)
|
||||
a.setChecked(self._model.active_column_index(column) is not None)
|
||||
a.setChecked(self._model.active_column_index(key) is not None)
|
||||
qconnect(
|
||||
a.toggled,
|
||||
lambda checked, column=column: self._on_column_toggled(checked, column),
|
||||
lambda checked, key=key: self._on_column_toggled(checked, key),
|
||||
)
|
||||
gui_hooks.browser_header_will_show_context_menu(self.browser, m)
|
||||
m.exec_(gpos)
|
||||
@ -375,16 +384,18 @@ class Table:
|
||||
if checked:
|
||||
self._scroll_to_column(self._model.len_columns() - 1)
|
||||
|
||||
def _on_sort_column_changed(self, index: int, order: int) -> None:
|
||||
def _on_sort_column_changed(self, section: int, order: int) -> None:
|
||||
order = bool(order)
|
||||
sort_column = self._model.active_column(index)
|
||||
if sort_column in ("question", "answer"):
|
||||
column = self._model.column_at_section(section)
|
||||
if column.sorting == Columns.SORTING_NONE:
|
||||
showInfo(tr.browsing_sorting_on_this_column_is_not())
|
||||
sort_column = self._state.sort_column
|
||||
if self._state.sort_column != sort_column:
|
||||
self._state.sort_column = sort_column
|
||||
sort_key = self._state.sort_column
|
||||
else:
|
||||
sort_key = column.key
|
||||
if self._state.sort_column != sort_key:
|
||||
self._state.sort_column = sort_key
|
||||
# default to descending for non-text fields
|
||||
if sort_column == "noteFld":
|
||||
if column.sorting == Columns.SORTING_REVERSED:
|
||||
order = not order
|
||||
self._state.sort_backwards = order
|
||||
self.browser.search()
|
||||
@ -523,7 +534,7 @@ class Table:
|
||||
|
||||
|
||||
class ItemState(ABC):
|
||||
_columns: List[Tuple[str, str]]
|
||||
config_key_prefix: str
|
||||
_active_columns: List[str]
|
||||
_sort_column: str
|
||||
_sort_backwards: bool
|
||||
@ -545,14 +556,18 @@ class ItemState(ABC):
|
||||
def card_ids_from_note_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
|
||||
return self.col.db.list(f"select id from cards where nid in {ids2str(items)}")
|
||||
|
||||
def column_key_at(self, index: int) -> str:
|
||||
return self._active_columns[index]
|
||||
|
||||
def column_label(self, column: Column) -> str:
|
||||
return (
|
||||
column.notes_mode_label if self.is_notes_mode() else column.cards_mode_label
|
||||
)
|
||||
|
||||
# Columns and sorting
|
||||
|
||||
# abstractproperty is deprecated but used due to mypy limitations
|
||||
# (https://github.com/python/mypy/issues/1362)
|
||||
@abstractproperty
|
||||
def columns(self) -> List[Tuple[str, str]]:
|
||||
"""Return all for the state available columns."""
|
||||
|
||||
@abstractproperty
|
||||
def active_columns(self) -> List[str]:
|
||||
"""Return the saved or default columns for the state."""
|
||||
@ -590,7 +605,9 @@ class ItemState(ABC):
|
||||
# Get ids
|
||||
|
||||
@abstractmethod
|
||||
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
|
||||
def find_items(
|
||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
||||
) -> Sequence[ItemId]:
|
||||
"""Return the item ids fitting the given search and order."""
|
||||
|
||||
@abstractmethod
|
||||
@ -619,37 +636,13 @@ class ItemState(ABC):
|
||||
class CardState(ItemState):
|
||||
def __init__(self, col: Collection) -> None:
|
||||
super().__init__(col)
|
||||
self._load_columns()
|
||||
self.config_key_prefix = "editor"
|
||||
self._active_columns = self.col.load_browser_card_columns()
|
||||
self._sort_column = self.col.get_config("sortType")
|
||||
self._sort_backwards = self.col.get_config_bool(
|
||||
Config.Bool.BROWSER_SORT_BACKWARDS
|
||||
)
|
||||
|
||||
def _load_columns(self) -> None:
|
||||
self._columns = [
|
||||
("question", tr.browsing_question()),
|
||||
("answer", tr.browsing_answer()),
|
||||
("template", tr.browsing_card()),
|
||||
("deck", tr.decks_deck()),
|
||||
("noteFld", tr.browsing_sort_field()),
|
||||
("noteCrt", tr.browsing_created()),
|
||||
("noteMod", tr.search_note_modified()),
|
||||
("cardMod", tr.search_card_modified()),
|
||||
("cardDue", tr.statistics_due_date()),
|
||||
("cardIvl", tr.browsing_interval()),
|
||||
("cardEase", tr.browsing_ease()),
|
||||
("cardReps", tr.scheduling_reviews()),
|
||||
("cardLapses", tr.scheduling_lapses()),
|
||||
("noteTags", tr.editing_tags()),
|
||||
("note", tr.browsing_note()),
|
||||
]
|
||||
self._columns.sort(key=itemgetter(1))
|
||||
|
||||
@property
|
||||
def columns(self) -> List[Tuple[str, str]]:
|
||||
return self._columns
|
||||
|
||||
@property
|
||||
def active_columns(self) -> List[str]:
|
||||
return self._active_columns
|
||||
@ -685,8 +678,10 @@ class CardState(ItemState):
|
||||
def get_note(self, item: ItemId) -> Note:
|
||||
return self.get_card(item).note()
|
||||
|
||||
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
|
||||
return self.col.find_cards(search, order)
|
||||
def find_items(
|
||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
||||
) -> Sequence[ItemId]:
|
||||
return self.col.find_cards(search, order, reverse)
|
||||
|
||||
def get_item_from_card_id(self, card: CardId) -> ItemId:
|
||||
return card
|
||||
@ -707,33 +702,13 @@ class CardState(ItemState):
|
||||
class NoteState(ItemState):
|
||||
def __init__(self, col: Collection) -> None:
|
||||
super().__init__(col)
|
||||
self._load_columns()
|
||||
self.config_key_prefix = "editorNotesMode"
|
||||
self._active_columns = self.col.load_browser_note_columns()
|
||||
self._sort_column = self.col.get_config("noteSortType")
|
||||
self._sort_backwards = self.col.get_config_bool(
|
||||
Config.Bool.BROWSER_NOTE_SORT_BACKWARDS
|
||||
)
|
||||
|
||||
def _load_columns(self) -> None:
|
||||
self._columns = [
|
||||
("note", tr.browsing_note()),
|
||||
("noteCards", tr.editing_cards()),
|
||||
("noteCrt", tr.browsing_created()),
|
||||
("noteDue", tr.statistics_due_date()),
|
||||
("noteEase", tr.browsing_average_ease()),
|
||||
("noteFld", tr.browsing_sort_field()),
|
||||
("noteIvl", tr.browsing_average_interval()),
|
||||
("noteLapses", tr.scheduling_lapses()),
|
||||
("noteMod", tr.search_note_modified()),
|
||||
("noteReps", tr.scheduling_reviews()),
|
||||
("noteTags", tr.editing_tags()),
|
||||
]
|
||||
self._columns.sort(key=itemgetter(1))
|
||||
|
||||
@property
|
||||
def columns(self) -> List[Tuple[str, str]]:
|
||||
return self._columns
|
||||
|
||||
@property
|
||||
def active_columns(self) -> List[str]:
|
||||
return self._active_columns
|
||||
@ -769,11 +744,13 @@ class NoteState(ItemState):
|
||||
def get_note(self, item: ItemId) -> Note:
|
||||
return self.col.get_note(NoteId(item))
|
||||
|
||||
def find_items(self, search: str, order: Union[bool, str]) -> Sequence[ItemId]:
|
||||
return self.col.find_notes(search, order)
|
||||
def find_items(
|
||||
self, search: str, order: Union[bool, str, Column], reverse: bool
|
||||
) -> Sequence[ItemId]:
|
||||
return self.col.find_notes(search, order, reverse)
|
||||
|
||||
def get_item_from_card_id(self, card: CardId) -> ItemId:
|
||||
return self.get_card(card).note().id
|
||||
return self.col.get_card(card).note().id
|
||||
|
||||
def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
|
||||
return super().card_ids_from_note_ids(items)
|
||||
@ -854,13 +831,27 @@ def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str,
|
||||
|
||||
|
||||
class DataModel(QAbstractTableModel):
|
||||
"""Data manager for the browser table.
|
||||
|
||||
_items -- The card or note ids currently hold and corresponding to the
|
||||
table's rows.
|
||||
_rows -- The cached data objects to render items to rows.
|
||||
columns -- The data objects of all available columns, used to define the display
|
||||
of active columns and list all toggleable columns to the user.
|
||||
_block_updates -- If True, serve stale content to avoid hitting the DB.
|
||||
_stale_cutoff -- A threshold to decide whether a cached row has gone stale.
|
||||
"""
|
||||
|
||||
def __init__(self, col: Collection, state: ItemState) -> None:
|
||||
QAbstractTableModel.__init__(self)
|
||||
self.col: Collection = col
|
||||
self.columns: Dict[str, Column] = dict(
|
||||
((c.key, c) for c in self.col.all_browser_columns())
|
||||
)
|
||||
gui_hooks.browser_did_fetch_columns(self.columns)
|
||||
self._state: ItemState = state
|
||||
self._items: Sequence[ItemId] = []
|
||||
self._rows: Dict[int, CellRow] = {}
|
||||
# serve stale content to avoid hitting the DB?
|
||||
self._block_updates = False
|
||||
self._stale_cutoff = 0.0
|
||||
|
||||
@ -1010,9 +1001,18 @@ class DataModel(QAbstractTableModel):
|
||||
def search(self, context: SearchContext) -> None:
|
||||
self.begin_reset()
|
||||
try:
|
||||
if context.order is True:
|
||||
try:
|
||||
context.order = self.columns[self._state.sort_column]
|
||||
except KeyError:
|
||||
# invalid sort column in config
|
||||
context.order = self.columns["noteCrt"]
|
||||
context.reverse = self._state.sort_backwards
|
||||
gui_hooks.browser_will_search(context)
|
||||
if context.ids is None:
|
||||
context.ids = self._state.find_items(context.search, context.order)
|
||||
context.ids = self._state.find_items(
|
||||
context.search, context.order, context.reverse
|
||||
)
|
||||
gui_hooks.browser_did_search(context)
|
||||
self._items = context.ids
|
||||
self._rows = {}
|
||||
@ -1026,8 +1026,19 @@ class DataModel(QAbstractTableModel):
|
||||
|
||||
# Columns
|
||||
|
||||
def active_column(self, index: int) -> str:
|
||||
return self._state.active_columns[index]
|
||||
def column_at(self, index: QModelIndex) -> Column:
|
||||
return self.column_at_section(index.column())
|
||||
|
||||
def column_at_section(self, section: int) -> Column:
|
||||
"""Returns the column object corresponding to the active column at index or the default
|
||||
column object if no data is associated with the active column.
|
||||
"""
|
||||
key = self._state.column_key_at(section)
|
||||
try:
|
||||
return self.columns[key]
|
||||
except KeyError:
|
||||
self.columns[key] = addon_column_fillin(key)
|
||||
return self.columns[key]
|
||||
|
||||
def active_column_index(self, column: str) -> Optional[int]:
|
||||
return (
|
||||
@ -1058,11 +1069,7 @@ class DataModel(QAbstractTableModel):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
if role == Qt.FontRole:
|
||||
if self.active_column(index.column()) not in (
|
||||
"question",
|
||||
"answer",
|
||||
"noteFld",
|
||||
):
|
||||
if not self.column_at(index).uses_cell_font:
|
||||
return QVariant()
|
||||
qfont = QFont()
|
||||
row = self.get_row(index)
|
||||
@ -1071,15 +1078,7 @@ class DataModel(QAbstractTableModel):
|
||||
return qfont
|
||||
if role == Qt.TextAlignmentRole:
|
||||
align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter
|
||||
if self.active_column(index.column()) not in (
|
||||
"question",
|
||||
"answer",
|
||||
"template",
|
||||
"deck",
|
||||
"noteFld",
|
||||
"note",
|
||||
"noteTags",
|
||||
):
|
||||
if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER:
|
||||
align |= Qt.AlignHCenter
|
||||
return align
|
||||
if role in (Qt.DisplayRole, Qt.ToolTipRole):
|
||||
@ -1089,21 +1088,9 @@ class DataModel(QAbstractTableModel):
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role: int = 0
|
||||
) -> Optional[str]:
|
||||
if orientation == Qt.Vertical:
|
||||
return None
|
||||
elif role == Qt.DisplayRole and section < self.len_columns():
|
||||
column = self.active_column(section)
|
||||
txt = None
|
||||
for stype, name in self._state.columns:
|
||||
if column == stype:
|
||||
txt = name
|
||||
break
|
||||
# give the user a hint an invalid column was added by an add-on
|
||||
if not txt:
|
||||
txt = tr.browsing_addon()
|
||||
return txt
|
||||
else:
|
||||
return None
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self._state.column_label(self.column_at_section(section))
|
||||
return None
|
||||
|
||||
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||
if self.get_row(index).is_deleted:
|
||||
@ -1131,3 +1118,17 @@ class StatusDelegate(QItemDelegate):
|
||||
painter.fillRect(option.rect, brush)
|
||||
painter.restore()
|
||||
return QItemDelegate.paint(self, painter, option, index)
|
||||
|
||||
|
||||
def addon_column_fillin(key: str) -> Column:
|
||||
"""Return a column with generic fields and a label indicating to the user that this column was
|
||||
added by an add-on.
|
||||
"""
|
||||
return Column(
|
||||
key=key,
|
||||
cards_mode_label=tr.browsing_addon(),
|
||||
notes_mode_label=tr.browsing_addon(),
|
||||
sorting=Columns.SORTING_NONE,
|
||||
uses_cell_font=False,
|
||||
alignment=Columns.ALIGNMENT_CENTER,
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ prefix = """\
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, List, Sequence, Tuple, Optional, Union
|
||||
from typing import Any, Callable, Dict, List, Sequence, Tuple, Optional, Union
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
@ -422,6 +422,18 @@ hooks = [
|
||||
represents.
|
||||
""",
|
||||
),
|
||||
Hook(
|
||||
name="browser_did_fetch_columns",
|
||||
args=["columns: Dict[str, aqt.table.Column]"],
|
||||
doc="""Allows you to add custom columns to the browser.
|
||||
|
||||
columns is a dictionary of data obejcts. You can add an entry with a custom
|
||||
column to describe how it should be displayed in the browser or modify
|
||||
existing entries.
|
||||
|
||||
Every column in the dictionary will be toggleable by the user.
|
||||
""",
|
||||
),
|
||||
# Main window states
|
||||
###################
|
||||
# these refer to things like deckbrowser, overview and reviewer state,
|
||||
|
@ -250,9 +250,9 @@ service SearchService {
|
||||
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
|
||||
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
|
||||
rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount);
|
||||
rpc AllBrowserColumns(Empty) returns (BrowserColumns);
|
||||
rpc BrowserRowForId(Int64) returns (BrowserRow);
|
||||
rpc SetDesktopBrowserCardColumns(StringList) returns (Empty);
|
||||
rpc SetDesktopBrowserNoteColumns(StringList) returns (Empty);
|
||||
rpc SetActiveBrowserColumns(StringList) returns (Empty);
|
||||
}
|
||||
|
||||
service StatsService {
|
||||
@ -797,35 +797,13 @@ message SearchOut {
|
||||
|
||||
message SortOrder {
|
||||
message Builtin {
|
||||
enum Kind {
|
||||
NOTE_CARDS = 0;
|
||||
NOTE_CREATION = 1;
|
||||
NOTE_DUE = 2;
|
||||
NOTE_EASE = 3;
|
||||
NOTE_FIELD = 4;
|
||||
NOTE_INTERVAL = 5;
|
||||
NOTE_LAPSES = 6;
|
||||
NOTE_MOD = 7;
|
||||
NOTE_REPS = 8;
|
||||
NOTE_TAGS = 9;
|
||||
NOTETYPE = 10;
|
||||
CARD_MOD = 11;
|
||||
CARD_REPS = 12;
|
||||
CARD_DUE = 13;
|
||||
CARD_EASE = 14;
|
||||
CARD_LAPSES = 15;
|
||||
CARD_INTERVAL = 16;
|
||||
CARD_DECK = 17;
|
||||
CARD_TEMPLATE = 18;
|
||||
}
|
||||
Kind kind = 1;
|
||||
string column = 1;
|
||||
bool reverse = 2;
|
||||
}
|
||||
oneof value {
|
||||
Empty from_config = 1;
|
||||
Empty none = 2;
|
||||
string custom = 3;
|
||||
Builtin builtin = 4;
|
||||
Empty none = 1;
|
||||
string custom = 2;
|
||||
Builtin builtin = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1054,6 +1032,27 @@ message FindAndReplaceIn {
|
||||
string field_name = 6;
|
||||
}
|
||||
|
||||
message BrowserColumns {
|
||||
enum Sorting {
|
||||
SORTING_NONE = 0;
|
||||
SORTING_NORMAL = 1;
|
||||
SORTING_REVERSED = 2;
|
||||
}
|
||||
enum Alignment {
|
||||
ALIGNMENT_START = 0;
|
||||
ALIGNMENT_CENTER = 1;
|
||||
}
|
||||
message Column {
|
||||
string key = 1;
|
||||
string cards_mode_label = 2;
|
||||
string notes_mode_label = 3;
|
||||
Sorting sorting = 4;
|
||||
bool uses_cell_font = 5;
|
||||
Alignment alignment = 6;
|
||||
}
|
||||
repeated Column columns = 1;
|
||||
}
|
||||
|
||||
message BrowserRow {
|
||||
message Cell {
|
||||
string text = 1;
|
||||
|
@ -1,73 +1,29 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{backend_proto as pb, browser_table};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{backend_proto as pb, browser_table, i18n::I18n};
|
||||
|
||||
impl browser_table::Column {
|
||||
pub fn to_pb_column(self, i18n: &I18n) -> pb::browser_columns::Column {
|
||||
pb::browser_columns::Column {
|
||||
key: self.to_string(),
|
||||
cards_mode_label: self.cards_mode_label(i18n),
|
||||
notes_mode_label: self.notes_mode_label(i18n),
|
||||
sorting: self.sorting() as i32,
|
||||
uses_cell_font: self.uses_cell_font(),
|
||||
alignment: self.alignment() as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::StringList> for Vec<browser_table::Column> {
|
||||
fn from(input: pb::StringList) -> Self {
|
||||
input.vals.into_iter().map(Into::into).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for browser_table::Column {
|
||||
fn from(text: String) -> Self {
|
||||
match text.as_str() {
|
||||
"question" => browser_table::Column::Question,
|
||||
"answer" => browser_table::Column::Answer,
|
||||
"deck" => browser_table::Column::CardDeck,
|
||||
"cardDue" => browser_table::Column::CardDue,
|
||||
"cardEase" => browser_table::Column::CardEase,
|
||||
"cardLapses" => browser_table::Column::CardLapses,
|
||||
"cardIvl" => browser_table::Column::CardInterval,
|
||||
"cardMod" => browser_table::Column::CardMod,
|
||||
"cardReps" => browser_table::Column::CardReps,
|
||||
"template" => browser_table::Column::CardTemplate,
|
||||
"noteCards" => browser_table::Column::NoteCards,
|
||||
"noteCrt" => browser_table::Column::NoteCreation,
|
||||
"noteDue" => browser_table::Column::NoteDue,
|
||||
"noteEase" => browser_table::Column::NoteEase,
|
||||
"noteFld" => browser_table::Column::NoteField,
|
||||
"noteIvl" => browser_table::Column::NoteInterval,
|
||||
"noteLapses" => browser_table::Column::NoteLapses,
|
||||
"noteMod" => browser_table::Column::NoteMod,
|
||||
"noteReps" => browser_table::Column::NoteReps,
|
||||
"noteTags" => browser_table::Column::NoteTags,
|
||||
"note" => browser_table::Column::Notetype,
|
||||
_ => browser_table::Column::Custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<browser_table::Row> for pb::BrowserRow {
|
||||
fn from(row: browser_table::Row) -> Self {
|
||||
pb::BrowserRow {
|
||||
cells: row.cells.into_iter().map(Into::into).collect(),
|
||||
color: row.color.into(),
|
||||
font_name: row.font.name,
|
||||
font_size: row.font.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<browser_table::Cell> for pb::browser_row::Cell {
|
||||
fn from(cell: browser_table::Cell) -> Self {
|
||||
pb::browser_row::Cell {
|
||||
text: cell.text,
|
||||
is_rtl: cell.is_rtl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<browser_table::Color> for i32 {
|
||||
fn from(color: browser_table::Color) -> Self {
|
||||
match color {
|
||||
browser_table::Color::Default => pb::browser_row::Color::Default as i32,
|
||||
browser_table::Color::Marked => pb::browser_row::Color::Marked as i32,
|
||||
browser_table::Color::Suspended => pb::browser_row::Color::Suspended as i32,
|
||||
browser_table::Color::FlagRed => pb::browser_row::Color::FlagRed as i32,
|
||||
browser_table::Color::FlagOrange => pb::browser_row::Color::FlagOrange as i32,
|
||||
browser_table::Color::FlagGreen => pb::browser_row::Color::FlagGreen as i32,
|
||||
browser_table::Color::FlagBlue => pb::browser_row::Color::FlagBlue as i32,
|
||||
}
|
||||
input
|
||||
.vals
|
||||
.iter()
|
||||
.map(|c| browser_table::Column::from_str(c).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,13 @@
|
||||
mod browser_table;
|
||||
mod search_node;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::{convert::TryInto, str::FromStr, sync::Arc};
|
||||
|
||||
use super::Backend;
|
||||
use crate::{
|
||||
backend_proto as pb,
|
||||
backend_proto::{
|
||||
sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto,
|
||||
},
|
||||
config::SortKind,
|
||||
backend_proto::sort_order::Value as SortOrderProto,
|
||||
browser_table::Column,
|
||||
prelude::*,
|
||||
search::{concatenate_searches, replace_search_node, write_nodes, Node, SortMode},
|
||||
};
|
||||
@ -89,56 +87,31 @@ impl SearchService for Backend {
|
||||
})
|
||||
}
|
||||
|
||||
fn all_browser_columns(&self, _input: pb::Empty) -> Result<pb::BrowserColumns> {
|
||||
self.with_col(|col| Ok(col.all_browser_columns()))
|
||||
}
|
||||
|
||||
fn set_active_browser_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
|
||||
self.with_col(|col| {
|
||||
col.state.active_browser_columns = Some(Arc::new(input.into()));
|
||||
Ok(())
|
||||
})
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn browser_row_for_id(&self, input: pb::Int64) -> Result<pb::BrowserRow> {
|
||||
self.with_col(|col| col.browser_row_for_id(input.val).map(Into::into))
|
||||
}
|
||||
|
||||
fn set_desktop_browser_card_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
|
||||
self.with_col(|col| col.set_desktop_browser_card_columns(input.into()))?;
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
fn set_desktop_browser_note_columns(&self, input: pb::StringList) -> Result<pb::Empty> {
|
||||
self.with_col(|col| col.set_desktop_browser_note_columns(input.into()))?;
|
||||
Ok(().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortKindProto> for SortKind {
|
||||
fn from(kind: SortKindProto) -> Self {
|
||||
match kind {
|
||||
SortKindProto::NoteCards => SortKind::NoteCards,
|
||||
SortKindProto::NoteCreation => SortKind::NoteCreation,
|
||||
SortKindProto::NoteDue => SortKind::NoteDue,
|
||||
SortKindProto::NoteEase => SortKind::NoteEase,
|
||||
SortKindProto::NoteInterval => SortKind::NoteInterval,
|
||||
SortKindProto::NoteLapses => SortKind::NoteLapses,
|
||||
SortKindProto::NoteMod => SortKind::NoteMod,
|
||||
SortKindProto::NoteField => SortKind::NoteField,
|
||||
SortKindProto::NoteReps => SortKind::NoteReps,
|
||||
SortKindProto::NoteTags => SortKind::NoteTags,
|
||||
SortKindProto::Notetype => SortKind::Notetype,
|
||||
SortKindProto::CardMod => SortKind::CardMod,
|
||||
SortKindProto::CardReps => SortKind::CardReps,
|
||||
SortKindProto::CardDue => SortKind::CardDue,
|
||||
SortKindProto::CardEase => SortKind::CardEase,
|
||||
SortKindProto::CardLapses => SortKind::CardLapses,
|
||||
SortKindProto::CardInterval => SortKind::CardInterval,
|
||||
SortKindProto::CardDeck => SortKind::CardDeck,
|
||||
SortKindProto::CardTemplate => SortKind::CardTemplate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<SortOrderProto>> for SortMode {
|
||||
fn from(order: Option<SortOrderProto>) -> Self {
|
||||
use pb::sort_order::Value as V;
|
||||
match order.unwrap_or(V::FromConfig(pb::Empty {})) {
|
||||
match order.unwrap_or(V::None(pb::Empty {})) {
|
||||
V::None(_) => SortMode::NoOrder,
|
||||
V::Custom(s) => SortMode::Custom(s),
|
||||
V::FromConfig(_) => SortMode::FromConfig,
|
||||
V::Builtin(b) => SortMode::Builtin {
|
||||
kind: b.kind().into(),
|
||||
column: Column::from_str(&b.column).unwrap_or_default(),
|
||||
reverse: b.reverse,
|
||||
},
|
||||
}
|
||||
|
@ -4,11 +4,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
|
||||
|
||||
use crate::error::{AnkiError, Result};
|
||||
use crate::i18n::I18n;
|
||||
use crate::{
|
||||
backend_proto as pb,
|
||||
card::{Card, CardId, CardQueue, CardType},
|
||||
collection::Collection,
|
||||
config::BoolKey,
|
||||
@ -21,112 +22,47 @@ use crate::{
|
||||
timestamp::{TimestampMillis, TimestampSecs},
|
||||
};
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Display, EnumIter, EnumString)]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum Column {
|
||||
#[strum(serialize = "")]
|
||||
Custom,
|
||||
Question,
|
||||
Answer,
|
||||
CardDeck,
|
||||
CardDue,
|
||||
CardEase,
|
||||
CardLapses,
|
||||
CardInterval,
|
||||
CardMod,
|
||||
CardReps,
|
||||
CardTemplate,
|
||||
NoteCards,
|
||||
#[strum(serialize = "template")]
|
||||
Cards,
|
||||
Deck,
|
||||
#[strum(serialize = "cardDue")]
|
||||
Due,
|
||||
#[strum(serialize = "cardEase")]
|
||||
Ease,
|
||||
#[strum(serialize = "cardLapses")]
|
||||
Lapses,
|
||||
#[strum(serialize = "cardIvl")]
|
||||
Interval,
|
||||
#[strum(serialize = "noteCrt")]
|
||||
NoteCreation,
|
||||
NoteDue,
|
||||
NoteEase,
|
||||
NoteField,
|
||||
NoteInterval,
|
||||
NoteLapses,
|
||||
NoteMod,
|
||||
NoteReps,
|
||||
NoteTags,
|
||||
#[strum(serialize = "note")]
|
||||
Notetype,
|
||||
Question,
|
||||
#[strum(serialize = "cardReps")]
|
||||
Reps,
|
||||
#[strum(serialize = "noteFld")]
|
||||
SortField,
|
||||
#[strum(serialize = "noteTags")]
|
||||
Tags,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Row {
|
||||
pub cells: Vec<Cell>,
|
||||
pub color: Color,
|
||||
pub font: Font,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Cell {
|
||||
pub text: String,
|
||||
pub is_rtl: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Color {
|
||||
Default,
|
||||
Marked,
|
||||
Suspended,
|
||||
FlagRed,
|
||||
FlagOrange,
|
||||
FlagGreen,
|
||||
FlagBlue,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Font {
|
||||
pub name: String,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
trait RowContext {
|
||||
fn get_cell_text(&mut self, column: Column) -> Result<String>;
|
||||
fn get_row_color(&self) -> Color;
|
||||
fn get_row_font(&self) -> Result<Font>;
|
||||
fn note(&self) -> &Note;
|
||||
fn notetype(&self) -> &Notetype;
|
||||
|
||||
fn get_cell(&mut self, column: Column) -> Result<Cell> {
|
||||
Ok(Cell {
|
||||
text: self.get_cell_text(column)?,
|
||||
is_rtl: self.get_is_rtl(column),
|
||||
})
|
||||
}
|
||||
|
||||
fn note_creation_str(&self) -> String {
|
||||
TimestampMillis(self.note().id.into())
|
||||
.as_secs()
|
||||
.date_string()
|
||||
}
|
||||
|
||||
fn note_field_str(&self) -> String {
|
||||
let index = self.notetype().config.sort_field_idx as usize;
|
||||
html_to_text_line(&self.note().fields()[index]).into()
|
||||
}
|
||||
|
||||
fn get_is_rtl(&self, column: Column) -> bool {
|
||||
match column {
|
||||
Column::NoteField => {
|
||||
let index = self.notetype().config.sort_field_idx as usize;
|
||||
self.notetype().fields[index].config.rtl
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_row_for_id(&mut self, columns: &[Column]) -> Result<Row> {
|
||||
Ok(Row {
|
||||
cells: columns
|
||||
.iter()
|
||||
.map(|&column| self.get_cell(column))
|
||||
.collect::<Result<_>>()?,
|
||||
color: self.get_row_color(),
|
||||
font: self.get_row_font()?,
|
||||
})
|
||||
impl Default for Column {
|
||||
fn default() -> Self {
|
||||
Column::Custom
|
||||
}
|
||||
}
|
||||
|
||||
struct CardRowContext {
|
||||
card: Card,
|
||||
struct RowContext {
|
||||
notes_mode: bool,
|
||||
cards: Vec<Card>,
|
||||
note: Note,
|
||||
notetype: Arc<Notetype>,
|
||||
deck: Arc<Deck>,
|
||||
@ -143,14 +79,6 @@ struct RenderContext {
|
||||
answer_nodes: Vec<RenderedNode>,
|
||||
}
|
||||
|
||||
struct NoteRowContext {
|
||||
note: Note,
|
||||
notetype: Arc<Notetype>,
|
||||
cards: Vec<Card>,
|
||||
tr: I18n,
|
||||
timing: SchedTimingToday,
|
||||
}
|
||||
|
||||
fn card_render_required(columns: &[Column]) -> bool {
|
||||
columns
|
||||
.iter()
|
||||
@ -200,20 +128,88 @@ impl Note {
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn browser_row_for_id(&mut self, id: i64) -> Result<Row> {
|
||||
if self.get_bool(BoolKey::BrowserTableShowNotesMode) {
|
||||
let columns = self
|
||||
.get_desktop_browser_note_columns()
|
||||
.ok_or_else(|| AnkiError::invalid_input("Note columns not set."))?;
|
||||
NoteRowContext::new(self, id)?.browser_row_for_id(&columns)
|
||||
} else {
|
||||
let columns = self
|
||||
.get_desktop_browser_card_columns()
|
||||
.ok_or_else(|| AnkiError::invalid_input("Card columns not set."))?;
|
||||
CardRowContext::new(self, id, card_render_required(&columns))?
|
||||
.browser_row_for_id(&columns)
|
||||
impl Column {
|
||||
pub fn cards_mode_label(self, i18n: &I18n) -> String {
|
||||
match self {
|
||||
Self::Answer => i18n.browsing_answer(),
|
||||
Self::CardMod => i18n.search_card_modified(),
|
||||
Self::Cards => i18n.browsing_card(),
|
||||
Self::Deck => i18n.decks_deck(),
|
||||
Self::Due => i18n.statistics_due_date(),
|
||||
Self::Custom => i18n.browsing_addon(),
|
||||
Self::Ease => i18n.browsing_ease(),
|
||||
Self::Interval => i18n.browsing_interval(),
|
||||
Self::Lapses => i18n.scheduling_lapses(),
|
||||
Self::NoteCreation => i18n.browsing_created(),
|
||||
Self::NoteMod => i18n.search_note_modified(),
|
||||
Self::Notetype => i18n.browsing_note(),
|
||||
Self::Question => i18n.browsing_question(),
|
||||
Self::Reps => i18n.scheduling_reviews(),
|
||||
Self::SortField => i18n.browsing_sort_field(),
|
||||
Self::Tags => i18n.editing_tags(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn notes_mode_label(self, i18n: &I18n) -> String {
|
||||
match self {
|
||||
Self::CardMod => i18n.search_card_modified(),
|
||||
Self::Cards => i18n.editing_cards(),
|
||||
Self::Ease => i18n.browsing_average_ease(),
|
||||
Self::Interval => i18n.browsing_average_interval(),
|
||||
Self::Reps => i18n.scheduling_reviews(),
|
||||
_ => return self.cards_mode_label(i18n),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn sorting(self) -> pb::browser_columns::Sorting {
|
||||
use pb::browser_columns::Sorting;
|
||||
match self {
|
||||
Self::Question | Self::Answer | Self::Custom => Sorting::None,
|
||||
Self::SortField => Sorting::Reversed,
|
||||
_ => Sorting::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uses_cell_font(self) -> bool {
|
||||
matches!(self, Self::Question | Self::Answer | Self::SortField)
|
||||
}
|
||||
|
||||
pub fn alignment(self) -> pb::browser_columns::Alignment {
|
||||
use pb::browser_columns::Alignment;
|
||||
match self {
|
||||
Self::Question
|
||||
| Self::Answer
|
||||
| Self::Cards
|
||||
| Self::Deck
|
||||
| Self::SortField
|
||||
| Self::Notetype
|
||||
| Self::Tags => Alignment::Start,
|
||||
_ => Alignment::Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn all_browser_columns(&self) -> pb::BrowserColumns {
|
||||
let mut columns: Vec<pb::browser_columns::Column> = Column::iter()
|
||||
.filter(|&c| c != Column::Custom)
|
||||
.map(|c| c.to_pb_column(&self.tr))
|
||||
.collect();
|
||||
columns.sort_by(|c1, c2| c1.cards_mode_label.cmp(&c2.cards_mode_label));
|
||||
pb::BrowserColumns { columns }
|
||||
}
|
||||
|
||||
pub fn browser_row_for_id(&mut self, id: i64) -> Result<pb::BrowserRow> {
|
||||
let notes_mode = self.get_bool(BoolKey::BrowserTableShowNotesMode);
|
||||
let columns = Arc::clone(
|
||||
self.state
|
||||
.active_browser_columns
|
||||
.as_ref()
|
||||
.ok_or_else(|| AnkiError::invalid_input("Active browser columns not set."))?,
|
||||
);
|
||||
RowContext::new(self, id, notes_mode, card_render_required(&columns))?.browser_row(&columns)
|
||||
}
|
||||
|
||||
fn get_note_maybe_with_fields(&self, id: NoteId, _with_fields: bool) -> Result<Note> {
|
||||
@ -259,20 +255,32 @@ impl RenderContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl CardRowContext {
|
||||
fn new(col: &mut Collection, id: i64, with_card_render: bool) -> Result<Self> {
|
||||
let card = col
|
||||
.storage
|
||||
.get_card(CardId(id))?
|
||||
.ok_or(AnkiError::NotFound)?;
|
||||
let note = col.get_note_maybe_with_fields(card.note_id, with_card_render)?;
|
||||
impl RowContext {
|
||||
fn new(
|
||||
col: &mut Collection,
|
||||
id: i64,
|
||||
notes_mode: bool,
|
||||
with_card_render: bool,
|
||||
) -> Result<Self> {
|
||||
let cards;
|
||||
let note;
|
||||
if notes_mode {
|
||||
note = col.get_note_maybe_with_fields(NoteId(id), with_card_render)?;
|
||||
cards = col.storage.all_cards_of_note(note.id)?;
|
||||
} else {
|
||||
cards = vec![col
|
||||
.storage
|
||||
.get_card(CardId(id))?
|
||||
.ok_or(AnkiError::NotFound)?];
|
||||
note = col.get_note_maybe_with_fields(cards[0].note_id, with_card_render)?;
|
||||
}
|
||||
let notetype = col
|
||||
.get_notetype(note.notetype_id)?
|
||||
.ok_or(AnkiError::NotFound)?;
|
||||
let deck = col.get_deck(card.deck_id)?.ok_or(AnkiError::NotFound)?;
|
||||
let original_deck = if card.original_deck_id.0 != 0 {
|
||||
let deck = col.get_deck(cards[0].deck_id)?.ok_or(AnkiError::NotFound)?;
|
||||
let original_deck = if cards[0].original_deck_id.0 != 0 {
|
||||
Some(
|
||||
col.get_deck(card.original_deck_id)?
|
||||
col.get_deck(cards[0].original_deck_id)?
|
||||
.ok_or(AnkiError::NotFound)?,
|
||||
)
|
||||
} else {
|
||||
@ -280,13 +288,14 @@ impl CardRowContext {
|
||||
};
|
||||
let timing = col.timing_today()?;
|
||||
let render_context = if with_card_render {
|
||||
Some(RenderContext::new(col, &card, ¬e, ¬etype)?)
|
||||
Some(RenderContext::new(col, &cards[0], ¬e, ¬etype)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(CardRowContext {
|
||||
card,
|
||||
Ok(RowContext {
|
||||
notes_mode,
|
||||
cards,
|
||||
note,
|
||||
notetype,
|
||||
deck,
|
||||
@ -297,8 +306,67 @@ impl CardRowContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn browser_row(&self, columns: &[Column]) -> Result<pb::BrowserRow> {
|
||||
Ok(pb::BrowserRow {
|
||||
cells: columns
|
||||
.iter()
|
||||
.map(|&column| self.get_cell(column))
|
||||
.collect::<Result<_>>()?,
|
||||
color: self.get_row_color() as i32,
|
||||
font_name: self.get_row_font_name()?,
|
||||
font_size: self.get_row_font_size()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_cell(&self, column: Column) -> Result<pb::browser_row::Cell> {
|
||||
Ok(pb::browser_row::Cell {
|
||||
text: self.get_cell_text(column)?,
|
||||
is_rtl: self.get_is_rtl(column),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_cell_text(&self, column: Column) -> Result<String> {
|
||||
Ok(match column {
|
||||
Column::Question => self.question_str(),
|
||||
Column::Answer => self.answer_str(),
|
||||
Column::Deck => self.deck_str(),
|
||||
Column::Due => self.due_str(),
|
||||
Column::Ease => self.ease_str(),
|
||||
Column::Interval => self.interval_str(),
|
||||
Column::Lapses => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
|
||||
Column::CardMod => self.card_mod_str(),
|
||||
Column::Reps => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
|
||||
Column::Cards => self.cards_str()?,
|
||||
Column::NoteCreation => self.note_creation_str(),
|
||||
Column::SortField => self.note_field_str(),
|
||||
Column::NoteMod => self.note.mtime.date_string(),
|
||||
Column::Tags => self.note.tags.join(" "),
|
||||
Column::Notetype => self.notetype.name.to_owned(),
|
||||
Column::Custom => "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn note_creation_str(&self) -> String {
|
||||
TimestampMillis(self.note.id.into()).as_secs().date_string()
|
||||
}
|
||||
|
||||
fn note_field_str(&self) -> String {
|
||||
let index = self.notetype.config.sort_field_idx as usize;
|
||||
html_to_text_line(&self.note.fields()[index]).into()
|
||||
}
|
||||
|
||||
fn get_is_rtl(&self, column: Column) -> bool {
|
||||
match column {
|
||||
Column::SortField => {
|
||||
let index = self.notetype.config.sort_field_idx as usize;
|
||||
self.notetype.fields[index].config.rtl
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn template(&self) -> Result<&CardTemplate> {
|
||||
self.notetype.get_template(self.card.template_idx)
|
||||
self.notetype.get_template(self.cards[0].template_idx)
|
||||
}
|
||||
|
||||
fn answer_str(&self) -> String {
|
||||
@ -326,149 +394,31 @@ impl CardRowContext {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn card_due_str(&mut self) -> String {
|
||||
let due = if self.card.is_filtered_deck() {
|
||||
fn due_str(&self) -> String {
|
||||
if self.notes_mode {
|
||||
self.note_due_str()
|
||||
} else {
|
||||
self.card_due_str()
|
||||
}
|
||||
}
|
||||
|
||||
fn card_due_str(&self) -> String {
|
||||
let due = if self.cards[0].is_filtered_deck() {
|
||||
self.tr.browsing_filtered()
|
||||
} else if self.card.is_new_type_or_queue() {
|
||||
self.tr.statistics_due_for_new_card(self.card.due)
|
||||
} else if let Some(time) = self.card.due_time(&self.timing) {
|
||||
} else if self.cards[0].is_new_type_or_queue() {
|
||||
self.tr.statistics_due_for_new_card(self.cards[0].due)
|
||||
} else if let Some(time) = self.cards[0].due_time(&self.timing) {
|
||||
time.date_string().into()
|
||||
} else {
|
||||
return "".into();
|
||||
};
|
||||
if self.card.is_undue_queue() {
|
||||
if self.cards[0].is_undue_queue() {
|
||||
format!("({})", due)
|
||||
} else {
|
||||
due.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn card_ease_str(&self) -> String {
|
||||
match self.card.ctype {
|
||||
CardType::New => self.tr.browsing_new().into(),
|
||||
_ => format!("{}%", self.card.ease_factor / 10),
|
||||
}
|
||||
}
|
||||
|
||||
fn card_interval_str(&self) -> String {
|
||||
match self.card.ctype {
|
||||
CardType::New => self.tr.browsing_new().into(),
|
||||
CardType::Learn => self.tr.browsing_learning().into(),
|
||||
_ => time_span((self.card.interval * 86400) as f32, &self.tr, false),
|
||||
}
|
||||
}
|
||||
|
||||
fn deck_str(&mut self) -> String {
|
||||
let deck_name = self.deck.human_name();
|
||||
if let Some(original_deck) = &self.original_deck {
|
||||
format!("{} ({})", &deck_name, &original_deck.human_name())
|
||||
} else {
|
||||
deck_name
|
||||
}
|
||||
}
|
||||
|
||||
fn template_str(&self) -> Result<String> {
|
||||
let name = &self.template()?.name;
|
||||
Ok(match self.notetype.config.kind() {
|
||||
NotetypeKind::Normal => name.to_owned(),
|
||||
NotetypeKind::Cloze => format!("{} {}", name, self.card.template_idx + 1),
|
||||
})
|
||||
}
|
||||
|
||||
fn question_str(&self) -> String {
|
||||
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl RowContext for CardRowContext {
|
||||
fn get_cell_text(&mut self, column: Column) -> Result<String> {
|
||||
Ok(match column {
|
||||
Column::Question => self.question_str(),
|
||||
Column::Answer => self.answer_str(),
|
||||
Column::CardDeck => self.deck_str(),
|
||||
Column::CardDue => self.card_due_str(),
|
||||
Column::CardEase => self.card_ease_str(),
|
||||
Column::CardInterval => self.card_interval_str(),
|
||||
Column::CardLapses => self.card.lapses.to_string(),
|
||||
Column::CardMod => self.card.mtime.date_string(),
|
||||
Column::CardReps => self.card.reps.to_string(),
|
||||
Column::CardTemplate => self.template_str()?,
|
||||
Column::NoteCreation => self.note_creation_str(),
|
||||
Column::NoteField => self.note_field_str(),
|
||||
Column::NoteMod => self.note.mtime.date_string(),
|
||||
Column::NoteTags => self.note.tags.join(" "),
|
||||
Column::Notetype => self.notetype.name.to_owned(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_row_color(&self) -> Color {
|
||||
match self.card.flags {
|
||||
1 => Color::FlagRed,
|
||||
2 => Color::FlagOrange,
|
||||
3 => Color::FlagGreen,
|
||||
4 => Color::FlagBlue,
|
||||
_ => {
|
||||
if self.note.is_marked() {
|
||||
Color::Marked
|
||||
} else if self.card.queue == CardQueue::Suspended {
|
||||
Color::Suspended
|
||||
} else {
|
||||
Color::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_row_font(&self) -> Result<Font> {
|
||||
Ok(Font {
|
||||
name: self.template()?.config.browser_font_name.to_owned(),
|
||||
size: self.template()?.config.browser_font_size,
|
||||
})
|
||||
}
|
||||
|
||||
fn note(&self) -> &Note {
|
||||
&self.note
|
||||
}
|
||||
|
||||
fn notetype(&self) -> &Notetype {
|
||||
&self.notetype
|
||||
}
|
||||
}
|
||||
|
||||
impl NoteRowContext {
|
||||
fn new(col: &mut Collection, id: i64) -> Result<Self> {
|
||||
let note = col.get_note_maybe_with_fields(NoteId(id), false)?;
|
||||
let notetype = col
|
||||
.get_notetype(note.notetype_id)?
|
||||
.ok_or(AnkiError::NotFound)?;
|
||||
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||
let timing = col.timing_today()?;
|
||||
|
||||
Ok(NoteRowContext {
|
||||
note,
|
||||
notetype,
|
||||
cards,
|
||||
tr: col.tr.clone(),
|
||||
timing,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the average ease of the non-new cards or a hint if there aren't any.
|
||||
fn note_ease_str(&self) -> String {
|
||||
let eases: Vec<u16> = self
|
||||
.cards
|
||||
.iter()
|
||||
.filter(|c| c.ctype != CardType::New)
|
||||
.map(|c| c.ease_factor)
|
||||
.collect();
|
||||
if eases.is_empty() {
|
||||
self.tr.browsing_new().into()
|
||||
} else {
|
||||
format!("{}%", eases.iter().sum::<u16>() / eases.len() as u16 / 10)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the due date of the next due card that is not in a filtered deck, new, suspended or
|
||||
/// buried or the empty string if there is no such card.
|
||||
fn note_due_str(&self) -> String {
|
||||
@ -481,9 +431,30 @@ impl NoteRowContext {
|
||||
.unwrap_or_else(|| "".into())
|
||||
}
|
||||
|
||||
/// Returns the average interval of the review and relearn cards or the empty string if there
|
||||
/// aren't any.
|
||||
fn note_interval_str(&self) -> String {
|
||||
/// Returns the average ease of the non-new cards or a hint if there aren't any.
|
||||
fn ease_str(&self) -> String {
|
||||
let eases: Vec<u16> = self
|
||||
.cards
|
||||
.iter()
|
||||
.filter(|c| c.ctype != CardType::New)
|
||||
.map(|c| c.ease_factor)
|
||||
.collect();
|
||||
if eases.is_empty() {
|
||||
self.tr.browsing_new().into()
|
||||
} else {
|
||||
format!("{}%", eases.iter().sum::<u16>() / eases.len() as u16 / 10)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the average interval of the review and relearn cards if there are any.
|
||||
fn interval_str(&self) -> String {
|
||||
if !self.notes_mode {
|
||||
match self.cards[0].ctype {
|
||||
CardType::New => return self.tr.browsing_new().into(),
|
||||
CardType::Learn => return self.tr.browsing_learning().into(),
|
||||
CardType::Review | CardType::Relearn => (),
|
||||
}
|
||||
}
|
||||
let intervals: Vec<u32> = self
|
||||
.cards
|
||||
.iter()
|
||||
@ -500,46 +471,79 @@ impl NoteRowContext {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RowContext for NoteRowContext {
|
||||
fn get_cell_text(&mut self, column: Column) -> Result<String> {
|
||||
Ok(match column {
|
||||
Column::NoteCards => self.cards.len().to_string(),
|
||||
Column::NoteCreation => self.note_creation_str(),
|
||||
Column::NoteDue => self.note_due_str(),
|
||||
Column::NoteEase => self.note_ease_str(),
|
||||
Column::NoteField => self.note_field_str(),
|
||||
Column::NoteInterval => self.note_interval_str(),
|
||||
Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
|
||||
Column::NoteMod => self.note.mtime.date_string(),
|
||||
Column::NoteReps => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
|
||||
Column::NoteTags => self.note.tags.join(" "),
|
||||
Column::Notetype => self.notetype.name.to_owned(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
fn card_mod_str(&self) -> String {
|
||||
self.cards
|
||||
.iter()
|
||||
.map(|c| c.mtime)
|
||||
.max()
|
||||
.unwrap()
|
||||
.date_string()
|
||||
}
|
||||
|
||||
fn get_row_color(&self) -> Color {
|
||||
if self.note.is_marked() {
|
||||
Color::Marked
|
||||
fn deck_str(&self) -> String {
|
||||
if self.notes_mode {
|
||||
let decks = self.cards.iter().map(|c| c.deck_id).unique().count();
|
||||
if decks > 1 {
|
||||
return format!("({})", decks);
|
||||
}
|
||||
}
|
||||
let deck_name = self.deck.human_name();
|
||||
if let Some(original_deck) = &self.original_deck {
|
||||
format!("{} ({})", &deck_name, &original_deck.human_name())
|
||||
} else {
|
||||
Color::Default
|
||||
deck_name
|
||||
}
|
||||
}
|
||||
|
||||
fn get_row_font(&self) -> Result<Font> {
|
||||
Ok(Font {
|
||||
name: "".to_owned(),
|
||||
size: 0,
|
||||
fn cards_str(&self) -> Result<String> {
|
||||
Ok(if self.notes_mode {
|
||||
self.cards.len().to_string()
|
||||
} else {
|
||||
let name = &self.template()?.name;
|
||||
match self.notetype.config.kind() {
|
||||
NotetypeKind::Normal => name.to_owned(),
|
||||
NotetypeKind::Cloze => format!("{} {}", name, self.cards[0].template_idx + 1),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn note(&self) -> &Note {
|
||||
&self.note
|
||||
fn question_str(&self) -> String {
|
||||
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
|
||||
}
|
||||
|
||||
fn notetype(&self) -> &Notetype {
|
||||
&self.notetype
|
||||
fn get_row_font_name(&self) -> Result<String> {
|
||||
Ok(self.template()?.config.browser_font_name.to_owned())
|
||||
}
|
||||
|
||||
fn get_row_font_size(&self) -> Result<u32> {
|
||||
Ok(self.template()?.config.browser_font_size)
|
||||
}
|
||||
|
||||
fn get_row_color(&self) -> pb::browser_row::Color {
|
||||
use pb::browser_row::Color;
|
||||
if self.notes_mode {
|
||||
if self.note.is_marked() {
|
||||
Color::Marked
|
||||
} else {
|
||||
Color::Default
|
||||
}
|
||||
} else {
|
||||
match self.cards[0].flags {
|
||||
1 => Color::FlagRed,
|
||||
2 => Color::FlagOrange,
|
||||
3 => Color::FlagGreen,
|
||||
4 => Color::FlagBlue,
|
||||
_ => {
|
||||
if self.note.is_marked() {
|
||||
Color::Marked
|
||||
} else if self.cards[0].queue == CardQueue::Suspended {
|
||||
Color::Suspended
|
||||
} else {
|
||||
Color::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
use crate::types::Usn;
|
||||
use crate::{
|
||||
browser_table,
|
||||
decks::{Deck, DeckId},
|
||||
notetype::{Notetype, NotetypeId},
|
||||
prelude::*,
|
||||
@ -66,6 +67,7 @@ pub struct CollectionState {
|
||||
pub(crate) deck_cache: HashMap<DeckId, Arc<Deck>>,
|
||||
pub(crate) scheduler_info: Option<SchedulerInfo>,
|
||||
pub(crate) card_queues: Option<CardQueues>,
|
||||
pub(crate) active_browser_columns: Option<Arc<Vec<browser_table::Column>>>,
|
||||
/// True if legacy Python code has executed SQL that has modified the
|
||||
/// database, requiring modification time to be bumped.
|
||||
pub(crate) modified_by_dbproxy: bool,
|
||||
|
@ -9,10 +9,8 @@ mod string;
|
||||
pub(crate) mod undo;
|
||||
|
||||
pub use self::{bool::BoolKey, string::StringKey};
|
||||
use crate::browser_table;
|
||||
use crate::prelude::*;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_derive::Deserialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use slog::warn;
|
||||
use strum::IntoStaticStr;
|
||||
@ -47,10 +45,6 @@ pub(crate) enum ConfigKey {
|
||||
|
||||
#[strum(to_string = "timeLim")]
|
||||
AnswerTimeLimitSecs,
|
||||
#[strum(to_string = "sortType")]
|
||||
BrowserSortKind,
|
||||
#[strum(to_string = "noteSortType")]
|
||||
BrowserNoteSortKind,
|
||||
#[strum(to_string = "curDeck")]
|
||||
CurrentDeckId,
|
||||
#[strum(to_string = "curModel")]
|
||||
@ -65,9 +59,6 @@ pub(crate) enum ConfigKey {
|
||||
NextNewCardPosition,
|
||||
#[strum(to_string = "schedVer")]
|
||||
SchedulerVersion,
|
||||
|
||||
DesktopBrowserCardColumns,
|
||||
DesktopBrowserNoteColumns,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
|
||||
@ -132,38 +123,6 @@ impl Collection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_browser_sort_kind(&self) -> SortKind {
|
||||
self.get_config_default(ConfigKey::BrowserSortKind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_browser_note_sort_kind(&self) -> SortKind {
|
||||
self.get_config_default(ConfigKey::BrowserNoteSortKind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_desktop_browser_card_columns(&self) -> Option<Vec<browser_table::Column>> {
|
||||
self.get_config_optional(ConfigKey::DesktopBrowserCardColumns)
|
||||
}
|
||||
|
||||
pub(crate) fn set_desktop_browser_card_columns(
|
||||
&mut self,
|
||||
columns: Vec<browser_table::Column>,
|
||||
) -> Result<()> {
|
||||
self.set_config(ConfigKey::DesktopBrowserCardColumns, &columns)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_desktop_browser_note_columns(&self) -> Option<Vec<browser_table::Column>> {
|
||||
self.get_config_optional(ConfigKey::DesktopBrowserNoteColumns)
|
||||
}
|
||||
|
||||
pub(crate) fn set_desktop_browser_note_columns(
|
||||
&mut self,
|
||||
columns: Vec<browser_table::Column>,
|
||||
) -> Result<()> {
|
||||
self.set_config(ConfigKey::DesktopBrowserNoteColumns, &columns)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
|
||||
self.get_config_optional(ConfigKey::CreationOffset)
|
||||
}
|
||||
@ -280,43 +239,6 @@ impl Collection {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, PartialEq, Debug, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SortKind {
|
||||
NoteCards,
|
||||
#[serde(rename = "noteCrt")]
|
||||
NoteCreation,
|
||||
NoteDue,
|
||||
NoteEase,
|
||||
#[serde(rename = "noteIvl")]
|
||||
NoteInterval,
|
||||
NoteLapses,
|
||||
NoteMod,
|
||||
#[serde(rename = "noteFld")]
|
||||
NoteField,
|
||||
NoteReps,
|
||||
#[serde(rename = "note")]
|
||||
Notetype,
|
||||
NoteTags,
|
||||
CardMod,
|
||||
CardReps,
|
||||
CardDue,
|
||||
CardEase,
|
||||
CardLapses,
|
||||
#[serde(rename = "cardIvl")]
|
||||
CardInterval,
|
||||
#[serde(rename = "deck")]
|
||||
CardDeck,
|
||||
#[serde(rename = "template")]
|
||||
CardTemplate,
|
||||
}
|
||||
|
||||
impl Default for SortKind {
|
||||
fn default() -> Self {
|
||||
Self::NoteCreation
|
||||
}
|
||||
}
|
||||
|
||||
// 2021 scheduler moves this into deck config
|
||||
pub(crate) enum NewReviewMix {
|
||||
Mix = 0,
|
||||
@ -341,7 +263,6 @@ pub(crate) enum Weekday {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SortKind;
|
||||
use crate::collection::open_test_collection;
|
||||
use crate::decks::DeckId;
|
||||
|
||||
@ -349,7 +270,6 @@ mod test {
|
||||
fn defaults() {
|
||||
let col = open_test_collection();
|
||||
assert_eq!(col.get_current_deck_id(), DeckId(1));
|
||||
assert_eq!(col.get_browser_sort_kind(), SortKind::NoteField);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
10
rslib/src/search/card_mod_order.sql
Normal file
10
rslib/src/search/card_mod_order.sql
Normal file
@ -0,0 +1,10 @@
|
||||
DROP TABLE IF EXISTS sort_order;
|
||||
CREATE TEMPORARY TABLE sort_order (
|
||||
pos integer PRIMARY KEY,
|
||||
nid integer NOT NULL UNIQUE
|
||||
);
|
||||
INSERT INTO sort_order (nid)
|
||||
SELECT nid
|
||||
FROM cards
|
||||
GROUP BY nid
|
||||
ORDER BY MAX(mod);
|
@ -14,19 +14,13 @@ use rusqlite::types::FromSql;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
card::CardId,
|
||||
card::CardType,
|
||||
collection::Collection,
|
||||
config::{BoolKey, SortKind},
|
||||
error::Result,
|
||||
notes::NoteId,
|
||||
prelude::AnkiError,
|
||||
search::parser::parse,
|
||||
browser_table::Column, card::CardId, card::CardType, collection::Collection, error::Result,
|
||||
notes::NoteId, prelude::AnkiError, search::parser::parse,
|
||||
};
|
||||
use sqlwriter::{RequiredTable, SqlWriter};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SearchItems {
|
||||
pub enum ReturnItemType {
|
||||
Cards,
|
||||
Notes,
|
||||
}
|
||||
@ -34,32 +28,31 @@ pub enum SearchItems {
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SortMode {
|
||||
NoOrder,
|
||||
FromConfig,
|
||||
Builtin { kind: SortKind, reverse: bool },
|
||||
Builtin { column: Column, reverse: bool },
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
pub trait AsSearchItems {
|
||||
fn as_search_items() -> SearchItems;
|
||||
pub trait AsReturnItemType {
|
||||
fn as_return_item_type() -> ReturnItemType;
|
||||
}
|
||||
|
||||
impl AsSearchItems for CardId {
|
||||
fn as_search_items() -> SearchItems {
|
||||
SearchItems::Cards
|
||||
impl AsReturnItemType for CardId {
|
||||
fn as_return_item_type() -> ReturnItemType {
|
||||
ReturnItemType::Cards
|
||||
}
|
||||
}
|
||||
|
||||
impl AsSearchItems for NoteId {
|
||||
fn as_search_items() -> SearchItems {
|
||||
SearchItems::Notes
|
||||
impl AsReturnItemType for NoteId {
|
||||
fn as_return_item_type() -> ReturnItemType {
|
||||
ReturnItemType::Notes
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchItems {
|
||||
impl ReturnItemType {
|
||||
fn required_table(&self) -> RequiredTable {
|
||||
match self {
|
||||
SearchItems::Cards => RequiredTable::Cards,
|
||||
SearchItems::Notes => RequiredTable::Notes,
|
||||
ReturnItemType::Cards => RequiredTable::Cards,
|
||||
ReturnItemType::Notes => RequiredTable::Notes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,8 +61,7 @@ impl SortMode {
|
||||
fn required_table(&self) -> RequiredTable {
|
||||
match self {
|
||||
SortMode::NoOrder => RequiredTable::CardsOrNotes,
|
||||
SortMode::FromConfig => unreachable!(),
|
||||
SortMode::Builtin { kind, .. } => kind.required_table(),
|
||||
SortMode::Builtin { column, .. } => column.required_table(),
|
||||
SortMode::Custom(ref text) => {
|
||||
if text.contains("n.") {
|
||||
if text.contains("c.") {
|
||||
@ -85,44 +77,31 @@ impl SortMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl SortKind {
|
||||
impl Column {
|
||||
fn required_table(self) -> RequiredTable {
|
||||
match self {
|
||||
SortKind::NoteCards
|
||||
| SortKind::NoteCreation
|
||||
| SortKind::NoteDue
|
||||
| SortKind::NoteEase
|
||||
| SortKind::NoteField
|
||||
| SortKind::NoteInterval
|
||||
| SortKind::NoteLapses
|
||||
| SortKind::NoteMod
|
||||
| SortKind::NoteReps
|
||||
| SortKind::NoteTags
|
||||
| SortKind::Notetype => RequiredTable::Notes,
|
||||
SortKind::CardTemplate => RequiredTable::CardsAndNotes,
|
||||
SortKind::CardMod
|
||||
| SortKind::CardReps
|
||||
| SortKind::CardDue
|
||||
| SortKind::CardEase
|
||||
| SortKind::CardLapses
|
||||
| SortKind::CardInterval
|
||||
| SortKind::CardDeck => RequiredTable::Cards,
|
||||
Column::Cards
|
||||
| Column::NoteCreation
|
||||
| Column::NoteMod
|
||||
| Column::Notetype
|
||||
| Column::SortField
|
||||
| Column::Tags => RequiredTable::Notes,
|
||||
_ => RequiredTable::CardsOrNotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn search<T>(&mut self, search: &str, mut mode: SortMode) -> Result<Vec<T>>
|
||||
pub fn search<T>(&mut self, search: &str, mode: SortMode) -> Result<Vec<T>>
|
||||
where
|
||||
T: FromSql + AsSearchItems,
|
||||
T: FromSql + AsReturnItemType,
|
||||
{
|
||||
let items = T::as_search_items();
|
||||
let item_type = T::as_return_item_type();
|
||||
let top_node = Node::Group(parse(search)?);
|
||||
self.resolve_config_sort(items, &mut mode);
|
||||
let writer = SqlWriter::new(self, items);
|
||||
let writer = SqlWriter::new(self, item_type);
|
||||
|
||||
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
||||
self.add_order(&mut sql, items, mode)?;
|
||||
self.add_order(&mut sql, item_type, mode)?;
|
||||
|
||||
let mut stmt = self.storage.db.prepare(&sql)?;
|
||||
let ids: Vec<_> = stmt
|
||||
@ -140,14 +119,18 @@ impl Collection {
|
||||
self.search(search, SortMode::NoOrder)
|
||||
}
|
||||
|
||||
fn add_order(&mut self, sql: &mut String, items: SearchItems, mode: SortMode) -> Result<()> {
|
||||
fn add_order(
|
||||
&mut self,
|
||||
sql: &mut String,
|
||||
item_type: ReturnItemType,
|
||||
mode: SortMode,
|
||||
) -> Result<()> {
|
||||
match mode {
|
||||
SortMode::NoOrder => (),
|
||||
SortMode::FromConfig => unreachable!(),
|
||||
SortMode::Builtin { kind, reverse } => {
|
||||
prepare_sort(self, kind)?;
|
||||
SortMode::Builtin { column, reverse } => {
|
||||
prepare_sort(self, column, item_type)?;
|
||||
sql.push_str(" order by ");
|
||||
write_order(sql, items, kind, reverse)?;
|
||||
write_order(sql, item_type, column, reverse)?;
|
||||
}
|
||||
SortMode::Custom(order_clause) => {
|
||||
sql.push_str(" order by ");
|
||||
@ -166,11 +149,11 @@ impl Collection {
|
||||
mode: SortMode,
|
||||
) -> Result<usize> {
|
||||
let top_node = Node::Group(parse(search)?);
|
||||
let writer = SqlWriter::new(self, SearchItems::Cards);
|
||||
let writer = SqlWriter::new(self, ReturnItemType::Cards);
|
||||
let want_order = mode != SortMode::NoOrder;
|
||||
|
||||
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
||||
self.add_order(&mut sql, SearchItems::Cards, mode)?;
|
||||
self.add_order(&mut sql, ReturnItemType::Cards, mode)?;
|
||||
|
||||
if want_order {
|
||||
self.storage
|
||||
@ -186,34 +169,23 @@ impl Collection {
|
||||
.execute(&args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// If the sort mode is based on a config setting, look it up.
|
||||
fn resolve_config_sort(&self, items: SearchItems, mode: &mut SortMode) {
|
||||
if mode == &SortMode::FromConfig {
|
||||
*mode = match items {
|
||||
SearchItems::Cards => SortMode::Builtin {
|
||||
kind: self.get_browser_sort_kind(),
|
||||
reverse: self.get_bool(BoolKey::BrowserSortBackwards),
|
||||
},
|
||||
SearchItems::Notes => SortMode::Builtin {
|
||||
kind: self.get_browser_note_sort_kind(),
|
||||
reverse: self.get_bool(BoolKey::BrowserNoteSortBackwards),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the order clause to the sql.
|
||||
fn write_order(sql: &mut String, items: SearchItems, kind: SortKind, reverse: bool) -> Result<()> {
|
||||
let order = match items {
|
||||
SearchItems::Cards => card_order_from_sortkind(kind),
|
||||
SearchItems::Notes => note_order_from_sortkind(kind),
|
||||
fn write_order(
|
||||
sql: &mut String,
|
||||
item_type: ReturnItemType,
|
||||
column: Column,
|
||||
reverse: bool,
|
||||
) -> Result<()> {
|
||||
let order = match item_type {
|
||||
ReturnItemType::Cards => card_order_from_sort_column(column),
|
||||
ReturnItemType::Notes => note_order_from_sort_column(column),
|
||||
};
|
||||
if order.is_empty() {
|
||||
return Err(AnkiError::invalid_input(format!(
|
||||
"Can't sort {:?} by {:?}.",
|
||||
items, kind
|
||||
item_type, column
|
||||
)));
|
||||
}
|
||||
if reverse {
|
||||
@ -229,60 +201,69 @@ fn write_order(sql: &mut String, items: SearchItems, kind: SortKind, reverse: bo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn card_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
|
||||
match kind {
|
||||
SortKind::NoteCreation => "n.id asc, c.ord asc".into(),
|
||||
SortKind::NoteMod => "n.mod asc, c.ord asc".into(),
|
||||
SortKind::NoteField => "n.sfld collate nocase asc, c.ord asc".into(),
|
||||
SortKind::CardMod => "c.mod asc".into(),
|
||||
SortKind::CardReps => "c.reps asc".into(),
|
||||
SortKind::CardDue => "c.type asc, c.due asc".into(),
|
||||
SortKind::CardEase => format!("c.type = {} asc, c.factor asc", CardType::New as i8).into(),
|
||||
SortKind::CardLapses => "c.lapses asc".into(),
|
||||
SortKind::CardInterval => "c.ivl asc".into(),
|
||||
SortKind::NoteTags => "n.tags asc".into(),
|
||||
SortKind::CardDeck => "(select pos from sort_order where did = c.did) asc".into(),
|
||||
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||
SortKind::CardTemplate => concat!(
|
||||
fn card_order_from_sort_column(column: Column) -> Cow<'static, str> {
|
||||
match column {
|
||||
Column::CardMod => "c.mod asc".into(),
|
||||
Column::Cards => concat!(
|
||||
"coalesce((select pos from sort_order where ntid = n.mid and ord = c.ord),",
|
||||
// need to fall back on ord 0 for cloze cards
|
||||
"(select pos from sort_order where ntid = n.mid and ord = 0)) asc"
|
||||
)
|
||||
.into(),
|
||||
_ => "".into(),
|
||||
Column::Deck => "(select pos from sort_order where did = c.did) asc".into(),
|
||||
Column::Due => "c.type asc, c.due asc".into(),
|
||||
Column::Ease => format!("c.type = {} asc, c.factor asc", CardType::New as i8).into(),
|
||||
Column::Interval => "c.ivl asc".into(),
|
||||
Column::Lapses => "c.lapses asc".into(),
|
||||
Column::NoteCreation => "n.id asc, c.ord asc".into(),
|
||||
Column::NoteMod => "n.mod asc, c.ord asc".into(),
|
||||
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||
Column::Reps => "c.reps asc".into(),
|
||||
Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(),
|
||||
Column::Tags => "n.tags asc".into(),
|
||||
Column::Answer | Column::Custom | Column::Question => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
|
||||
match kind {
|
||||
SortKind::NoteCards
|
||||
| SortKind::NoteDue
|
||||
| SortKind::NoteEase
|
||||
| SortKind::NoteInterval
|
||||
| SortKind::NoteLapses
|
||||
| SortKind::NoteReps => "(select pos from sort_order where nid = n.id) asc".into(),
|
||||
SortKind::NoteCreation => "n.id asc".into(),
|
||||
SortKind::NoteField => "n.sfld collate nocase asc".into(),
|
||||
SortKind::NoteMod => "n.mod asc".into(),
|
||||
SortKind::NoteTags => "n.tags asc".into(),
|
||||
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||
_ => "".into(),
|
||||
fn note_order_from_sort_column(column: Column) -> Cow<'static, str> {
|
||||
match column {
|
||||
Column::CardMod
|
||||
| Column::Cards
|
||||
| Column::Deck
|
||||
| Column::Due
|
||||
| Column::Ease
|
||||
| Column::Interval
|
||||
| Column::Lapses
|
||||
| Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(),
|
||||
Column::NoteCreation => "n.id asc".into(),
|
||||
Column::NoteMod => "n.mod asc".into(),
|
||||
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||
Column::SortField => "n.sfld collate nocase asc".into(),
|
||||
Column::Tags => "n.tags asc".into(),
|
||||
Column::Answer | Column::Custom | Column::Question => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
|
||||
use SortKind::*;
|
||||
let sql = match kind {
|
||||
CardDeck => include_str!("deck_order.sql"),
|
||||
CardTemplate => include_str!("template_order.sql"),
|
||||
NoteCards => include_str!("note_cards_order.sql"),
|
||||
NoteDue => include_str!("note_due_order.sql"),
|
||||
NoteEase => include_str!("note_ease_order.sql"),
|
||||
NoteInterval => include_str!("note_interval_order.sql"),
|
||||
NoteLapses => include_str!("note_lapses_order.sql"),
|
||||
NoteReps => include_str!("note_reps_order.sql"),
|
||||
Notetype => include_str!("notetype_order.sql"),
|
||||
_ => return Ok(()),
|
||||
fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType) -> Result<()> {
|
||||
let sql = match item_type {
|
||||
ReturnItemType::Cards => match column {
|
||||
Column::Cards => include_str!("template_order.sql"),
|
||||
Column::Deck => include_str!("deck_order.sql"),
|
||||
Column::Notetype => include_str!("notetype_order.sql"),
|
||||
_ => return Ok(()),
|
||||
},
|
||||
ReturnItemType::Notes => match column {
|
||||
Column::Cards => include_str!("note_cards_order.sql"),
|
||||
Column::CardMod => include_str!("card_mod_order.sql"),
|
||||
Column::Deck => include_str!("note_decks_order.sql"),
|
||||
Column::Due => include_str!("note_due_order.sql"),
|
||||
Column::Ease => include_str!("note_ease_order.sql"),
|
||||
Column::Interval => include_str!("note_interval_order.sql"),
|
||||
Column::Lapses => include_str!("note_lapses_order.sql"),
|
||||
Column::Reps => include_str!("note_reps_order.sql"),
|
||||
Column::Notetype => include_str!("notetype_order.sql"),
|
||||
_ => return Ok(()),
|
||||
},
|
||||
};
|
||||
|
||||
col.storage.db.execute_batch(sql)?;
|
||||
|
18
rslib/src/search/note_decks_order.sql
Normal file
18
rslib/src/search/note_decks_order.sql
Normal file
@ -0,0 +1,18 @@
|
||||
DROP TABLE IF EXISTS sort_order;
|
||||
CREATE TEMPORARY TABLE sort_order (
|
||||
pos integer PRIMARY KEY,
|
||||
nid integer NOT NULL UNIQUE
|
||||
);
|
||||
INSERT INTO sort_order (nid)
|
||||
SELECT nid
|
||||
FROM cards
|
||||
JOIN (
|
||||
SELECT id,
|
||||
row_number() OVER(
|
||||
ORDER BY name
|
||||
) AS pos
|
||||
FROM decks
|
||||
) decks ON cards.did = decks.id
|
||||
GROUP BY nid
|
||||
ORDER BY COUNT(DISTINCT did),
|
||||
decks.pos;
|
@ -3,7 +3,7 @@
|
||||
|
||||
use super::{
|
||||
parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind},
|
||||
SearchItems,
|
||||
ReturnItemType,
|
||||
};
|
||||
use crate::{
|
||||
card::{CardQueue, CardType},
|
||||
@ -25,24 +25,24 @@ use std::{borrow::Cow, fmt::Write};
|
||||
pub(crate) struct SqlWriter<'a> {
|
||||
col: &'a mut Collection,
|
||||
sql: String,
|
||||
items: SearchItems,
|
||||
item_type: ReturnItemType,
|
||||
args: Vec<String>,
|
||||
normalize_note_text: bool,
|
||||
table: RequiredTable,
|
||||
}
|
||||
|
||||
impl SqlWriter<'_> {
|
||||
pub(crate) fn new(col: &mut Collection, items: SearchItems) -> SqlWriter<'_> {
|
||||
pub(crate) fn new(col: &mut Collection, item_type: ReturnItemType) -> SqlWriter<'_> {
|
||||
let normalize_note_text = col.get_bool(BoolKey::NormalizeNoteText);
|
||||
let sql = String::new();
|
||||
let args = vec![];
|
||||
SqlWriter {
|
||||
col,
|
||||
sql,
|
||||
items,
|
||||
item_type,
|
||||
args,
|
||||
normalize_note_text,
|
||||
table: items.required_table(),
|
||||
table: item_type.required_table(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,9 +61,9 @@ impl SqlWriter<'_> {
|
||||
let sql = match self.table {
|
||||
RequiredTable::Cards => "select c.id from cards c where ",
|
||||
RequiredTable::Notes => "select n.id from notes n where ",
|
||||
_ => match self.items {
|
||||
SearchItems::Cards => "select c.id from cards c, notes n where c.nid=n.id and ",
|
||||
SearchItems::Notes => {
|
||||
_ => match self.item_type {
|
||||
ReturnItemType::Cards => "select c.id from cards c, notes n where c.nid=n.id and ",
|
||||
ReturnItemType::Notes => {
|
||||
"select distinct n.id from cards c, notes n where c.nid=n.id and "
|
||||
}
|
||||
},
|
||||
@ -588,7 +588,7 @@ mod test {
|
||||
// shortcut
|
||||
fn s(req: &mut Collection, search: &str) -> (String, Vec<String>) {
|
||||
let node = Node::Group(parse(search).unwrap());
|
||||
let mut writer = SqlWriter::new(req, SearchItems::Cards);
|
||||
let mut writer = SqlWriter::new(req, ReturnItemType::Cards);
|
||||
writer.table = RequiredTable::Notes.combine(node.required_table());
|
||||
writer.write_node_to_sql(&node).unwrap();
|
||||
(writer.sql, writer.args)
|
||||
|
Loading…
Reference in New Issue
Block a user