Merge pull request #1102 from RumovZ/more-browser

More browser fixes and features
This commit is contained in:
Damien Elmes 2021-03-30 19:27:55 +10:00 committed by GitHub
commit 325920aa23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 247 additions and 112 deletions

View file

@ -12,6 +12,8 @@ browsing-browser-appearance = Browser Appearance
browsing-browser-options = Browser Options browsing-browser-options = Browser Options
browsing-buried = Buried browsing-buried = Buried
browsing-card = Card browsing-card = Card
# Exactly one character representing 'Cards'; should differ from browsing-note-initial.
browsing-card-initial = C
browsing-card-list = Card List browsing-card-list = Card List
browsing-card-state = Card State browsing-card-state = Card State
browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck. browsing-cards-cant-be-manually-moved-into = Cards can't be manually moved into a filtered deck.
@ -63,6 +65,8 @@ browsing-new = (new)
browsing-new-note-type = New note type: browsing-new-note-type = New note type:
browsing-no-flag = No Flag browsing-no-flag = No Flag
browsing-note = Note browsing-note = Note
# Exactly one character representing 'Notes'; should differ from browsing-card-initial.
browsing-note-initial = N
browsing-notes-tagged = Notes tagged. browsing-notes-tagged = Notes tagged.
browsing-nothing = Nothing browsing-nothing = Nothing
browsing-only-new-cards-can-be-repositioned = Only new cards can be repositioned. browsing-only-new-cards-can-be-repositioned = Only new cards can be repositioned.

View file

@ -497,8 +497,22 @@ class Collection:
reverse: bool = False, reverse: bool = False,
) -> Sequence[CardId]: ) -> Sequence[CardId]:
"""Return card ids matching the provided search. """Return card ids matching the provided search.
To programmatically construct a search string, see .build_search_string(). To programmatically construct a search string, see .build_search_string().
To define a sort order, see _build_sort_mode().
If order=True, use the sort order stored in the collection config
If order=False, do no ordering
If order is a string, that text is added after 'order by' in the sql statement.
You must add ' asc' or ' desc' to the order, as Anki will replace asc with
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)
The reverse argument only applies when a BuiltinSort.Kind is provided;
otherwise the collection config defines whether reverse is set or not.
""" """
mode = _build_sort_mode(order, reverse) mode = _build_sort_mode(order, reverse)
return cast( return cast(
@ -512,8 +526,9 @@ class Collection:
reverse: bool = False, reverse: bool = False,
) -> Sequence[NoteId]: ) -> Sequence[NoteId]:
"""Return note ids matching the provided search. """Return note ids matching the provided search.
To programmatically construct a search string, see .build_search_string(). To programmatically construct a search string, see .build_search_string().
To define a sort order, see _build_sort_mode(). The order parameter is documented in .find_cards().
""" """
mode = _build_sort_mode(order, reverse) mode = _build_sort_mode(order, reverse)
return cast( return cast(
@ -1072,22 +1087,6 @@ def _build_sort_mode(
order: Union[bool, str, BuiltinSort.Kind.V], order: Union[bool, str, BuiltinSort.Kind.V],
reverse: bool, reverse: bool,
) -> _pb.SortOrder: ) -> _pb.SortOrder:
"""Return a SortOrder object for use in find_cards() or find_notes().
If order=True, use the sort order stored in the collection config
If order=False, do no ordering
If order is a string, that text is added after 'order by' in the sql statement.
You must add ' asc' or ' desc' to the order, as Anki will replace asc with
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)
The reverse argument only applies when a BuiltinSort.Kind is provided;
otherwise the collection config defines whether reverse is set or not.
"""
if isinstance(order, str): if isinstance(order, str):
return _pb.SortOrder(custom=order) return _pb.SortOrder(custom=order)
elif isinstance(order, bool): elif isinstance(order, bool):

View file

@ -37,6 +37,7 @@ from aqt.scheduling_ops import (
unsuspend_cards, unsuspend_cards,
) )
from aqt.sidebar import SidebarTreeView from aqt.sidebar import SidebarTreeView
from aqt.switch import Switch
from aqt.table import Table from aqt.table import Table
from aqt.tag_ops import add_tags, clear_unused_tags, remove_tags_for_notes from aqt.tag_ops import add_tags, clear_unused_tags, remove_tags_for_notes
from aqt.utils import ( from aqt.utils import (
@ -329,9 +330,9 @@ class Browser(QMainWindow):
selected = self.table.len_selection() selected = self.table.len_selection()
cur = self.table.len() cur = self.table.len()
tr_title = ( tr_title = (
tr.browsing_window_title tr.browsing_window_title_notes
if self.table.is_card_state() if self.table.is_notes_mode()
else tr.browsing_window_title_notes else tr.browsing_window_title
) )
self.setWindowTitle( self.setWindowTitle(
without_unicode_isolation(tr_title(total=cur, selected=selected)) without_unicode_isolation(tr_title(total=cur, selected=selected))
@ -374,10 +375,11 @@ class Browser(QMainWindow):
def setup_table(self) -> None: def setup_table(self) -> None:
self.table = Table(self) self.table = Table(self)
self.form.radio_cards.setChecked(self.table.is_card_state())
self.form.radio_notes.setChecked(not self.table.is_card_state())
self.table.set_view(self.form.tableView) self.table.set_view(self.form.tableView)
qconnect(self.form.radio_cards.toggled, self.on_table_state_changed) switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial())
switch.setChecked(self.table.is_notes_mode())
qconnect(switch.toggled, self.on_table_state_changed)
self.form.gridLayout.addWidget(switch, 0, 0)
def setupEditor(self) -> None: def setupEditor(self) -> None:
def add_preview_button(leftbuttons: List[str], editor: Editor) -> None: def add_preview_button(leftbuttons: List[str], editor: Editor) -> None:
@ -430,10 +432,10 @@ class Browser(QMainWindow):
self._update_flags_menu() self._update_flags_menu()
gui_hooks.browser_did_change_row(self) gui_hooks.browser_did_change_row(self)
@ensure_editor_saved_on_trigger @ensure_editor_saved
def on_table_state_changed(self) -> None: def on_table_state_changed(self, checked: bool) -> None:
self.mw.progress.start() self.mw.progress.start()
self.table.toggle_state(self.form.radio_cards.isChecked(), self._lastSearchTxt) self.table.toggle_state(checked, self._lastSearchTxt)
self.mw.progress.finish() self.mw.progress.finish()
# Sidebar # Sidebar

View file

@ -91,7 +91,7 @@
<property name="verticalSpacing"> <property name="verticalSpacing">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="0"> <item row="0" column="1">
<widget class="QComboBox" name="searchEdit"> <widget class="QComboBox" name="searchEdit">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -109,30 +109,6 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="view_state" stretch="0,1">
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QRadioButton" name="radio_cards">
<property name="text">
<string>qt_accel_cards</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_notes">
<property name="text">
<string>qt_accel_notes</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="QTableView" name="tableView"> <widget class="QTableView" name="tableView">
<property name="sizePolicy"> <property name="sizePolicy">

115
qt/aqt/switch.py Normal file
View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt import colors
from aqt.qt import *
from aqt.theme import theme_manager
class Switch(QAbstractButton):
"""A horizontal slider to toggle between two states which can be denoted by short strings.
The left state is the default and corresponds to isChecked=False.
"""
_margin: int = 2
def __init__(
self,
radius: int = 10,
left_label: str = "",
right_label: str = "",
parent: QWidget = None,
) -> None:
super().__init__(parent=parent)
self.setCheckable(True)
super().setChecked(False)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._left_label = left_label
self._right_label = right_label
self._path_radius = radius
self._knob_radius = radius - self._margin
self._left_position = self._position = self._path_radius + self._margin
self._right_position = 3 * self._path_radius + self._margin
@pyqtProperty(int) # type: ignore
def position(self) -> int:
return self._position
@position.setter # type: ignore
def position(self, position: int) -> None:
self._position = position
self.update()
@property
def start_position(self) -> int:
return self._left_position if self.isChecked() else self._right_position
@property
def end_position(self) -> int:
return self._right_position if self.isChecked() else self._left_position
@property
def label(self) -> str:
return self._right_label if self.isChecked() else self._left_label
def sizeHint(self) -> QSize:
return QSize(
4 * self._path_radius + 2 * self._margin,
2 * self._path_radius + 2 * self._margin,
)
def setChecked(self, checked: bool) -> None:
super().setChecked(checked)
self._position = self.end_position
self.update()
def paintEvent(self, _event: QPaintEvent) -> None:
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
self._paint_path(painter)
self._paint_knob(painter)
self._paint_label(painter)
def _paint_path(self, painter: QPainter) -> None:
painter.setBrush(QBrush(theme_manager.qcolor(colors.FRAME_BG)))
rectangle = QRectF(
self._margin,
self._margin,
self.width() - 2 * self._margin,
self.height() - 2 * self._margin,
)
painter.drawRoundedRect(rectangle, self._path_radius, self._path_radius)
def _current_knob_rectangle(self) -> QRectF:
return QRectF(
self.position - self._knob_radius, # type: ignore
2 * self._margin,
2 * self._knob_radius,
2 * self._knob_radius,
)
def _paint_knob(self, painter: QPainter) -> None:
painter.setBrush(QBrush(theme_manager.qcolor(colors.HIGHLIGHT_BG)))
painter.drawEllipse(self._current_knob_rectangle())
def _paint_label(self, painter: QPainter) -> None:
painter.setPen(QColor("white"))
font = painter.font()
font.setPixelSize(int(1.5 * self._knob_radius))
painter.setFont(font)
painter.drawText(self._current_knob_rectangle(), Qt.AlignCenter, self.label)
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton:
animation = QPropertyAnimation(self, b"position", self)
animation.setDuration(100)
animation.setStartValue(self.start_position)
animation.setEndValue(self.end_position)
animation.start()
def enterEvent(self, event: QEvent) -> None:
self.setCursor(Qt.PointingHandCursor)
super().enterEvent(event)

View file

@ -48,8 +48,7 @@ class SearchContext:
search: str search: str
browser: aqt.browser.Browser browser: aqt.browser.Browser
order: Union[bool, str] = True order: Union[bool, str] = True
# if set, provided card ids will be used instead of the regular search # if set, provided ids will be used instead of the regular search
# fixme: legacy support for card_ids?
ids: Optional[Sequence[ItemId]] = None ids: Optional[Sequence[ItemId]] = None
@ -94,8 +93,8 @@ class Table:
def has_next(self) -> bool: def has_next(self) -> bool:
return self.has_current() and self._current().row() < self.len() - 1 return self.has_current() and self._current().row() < self.len() - 1
def is_card_state(self) -> bool: def is_notes_mode(self) -> bool:
return self._state.is_card_state() return self._state.is_notes_mode()
# Get objects # Get objects
@ -194,15 +193,15 @@ class Table:
self._model.search(SearchContext(search=txt, browser=self.browser)) self._model.search(SearchContext(search=txt, browser=self.browser))
self._restore_selection(self._intersected_selection) self._restore_selection(self._intersected_selection)
def toggle_state(self, is_card_state: bool, last_search: str) -> None: def toggle_state(self, is_notes_mode: bool, last_search: str) -> None:
if is_card_state == self.is_card_state(): if is_notes_mode == self.is_notes_mode():
return return
self._save_selection() self._save_selection()
self._state = self._model.toggle_state( self._state = self._model.toggle_state(
SearchContext(search=last_search, browser=self.browser) SearchContext(search=last_search, browser=self.browser)
) )
self.col.set_config_bool( self.col.set_config_bool(
Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, not self.is_card_state() Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE, self.is_notes_mode()
) )
self._set_sort_indicator() self._set_sort_indicator()
self._set_column_sizes() self._set_column_sizes()
@ -328,14 +327,14 @@ class Table:
def _on_context_menu(self, _point: QPoint) -> None: def _on_context_menu(self, _point: QPoint) -> None:
menu = QMenu() menu = QMenu()
if self.is_card_state(): if self.is_notes_mode():
main = self.browser.form.menu_Cards
other = self.browser.form.menu_Notes
other_name = tr.qt_accel_notes()
else:
main = self.browser.form.menu_Notes main = self.browser.form.menu_Notes
other = self.browser.form.menu_Cards other = self.browser.form.menu_Cards
other_name = tr.qt_accel_cards() other_name = tr.qt_accel_cards()
else:
main = self.browser.form.menu_Cards
other = self.browser.form.menu_Notes
other_name = tr.qt_accel_notes()
for action in main.actions(): for action in main.actions():
menu.addAction(action) menu.addAction(action)
menu.addSeparator() menu.addSeparator()
@ -417,7 +416,8 @@ class Table:
current = current or rows[0] current = current or rows[0]
self._select_rows(rows) self._select_rows(rows)
self._set_current(current) self._set_current(current)
self._scroll_to_row(current) # editor may pop up and hide the row later on
QTimer.singleShot(100, lambda: self._scroll_to_row(current))
if self.len_selection() == 0: if self.len_selection() == 0:
# no row change will fire # no row change will fire
self.browser.onRowChanged(QItemSelection(), QItemSelection()) self.browser.onRowChanged(QItemSelection(), QItemSelection())
@ -431,7 +431,7 @@ class Table:
if rows: if rows:
if len(rows) < self.SELECTION_LIMIT: if len(rows) < self.SELECTION_LIMIT:
return rows return rows
if current in rows: if current and current in rows:
return [current] return [current]
return rows[0:1] return rows[0:1]
return [current if current else 0] return [current if current else 0]
@ -453,17 +453,19 @@ class Table:
selected_rows = self._model.get_item_rows( selected_rows = self._model.get_item_rows(
self._state.get_new_items(self._selected_items) self._state.get_new_items(self._selected_items)
) )
current_row = self._current_item and self._model.get_item_row( current_row = None
self._state.get_new_item(self._current_item) if self._current_item:
) if new_current := self._state.get_new_items([self._current_item]):
current_row = self._model.get_item_row(new_current[0])
return selected_rows, current_row return selected_rows, current_row
# Move # Move
def _scroll_to_row(self, row: int) -> None: def _scroll_to_row(self, row: int) -> None:
"""Scroll vertically to row.""" """Scroll vertically to row."""
position = self._view.rowViewportPosition(row) top_border = self._view.rowViewportPosition(row)
visible = 0 <= position < self._view.viewport().height() bottom_border = top_border + self._view.rowHeight(0)
visible = top_border >= 0 and bottom_border < self._view.viewport().height()
if not visible: if not visible:
horizontal = self._view.horizontalScrollBar().value() horizontal = self._view.horizontalScrollBar().value()
self._view.scrollTo(self._model.index(row, 0), self._view.PositionAtCenter) self._view.scrollTo(self._model.index(row, 0), self._view.PositionAtCenter)
@ -527,9 +529,9 @@ class ItemState(ABC):
def __init__(self, col: Collection) -> None: def __init__(self, col: Collection) -> None:
self.col = col self.col = col
def is_card_state(self) -> bool: def is_notes_mode(self) -> bool:
"""Return True if the state is a CardState.""" """Return True if the state is a NoteState."""
return isinstance(self, CardState) return isinstance(self, NoteState)
# Stateless Helpers # Stateless Helpers
@ -543,6 +545,8 @@ class ItemState(ABC):
# Columns and sorting # Columns and sorting
# abstractproperty is deprecated but used due to mypy limitations
# (https://github.com/python/mypy/issues/1362)
@abstractproperty @abstractproperty
def columns(self) -> List[Tuple[str, str]]: def columns(self) -> List[Tuple[str, str]]:
"""Return all for the state available columns.""" """Return all for the state available columns."""
@ -605,15 +609,9 @@ class ItemState(ABC):
def toggle_state(self) -> ItemState: def toggle_state(self) -> ItemState:
"""Return an instance of the other state.""" """Return an instance of the other state."""
@abstractmethod
def get_new_item(self, old_item: ItemId) -> ItemId:
"""Given an id from the other state, return the corresponding id for
this state."""
@abstractmethod @abstractmethod
def get_new_items(self, old_items: Sequence[ItemId]) -> ItemList: def get_new_items(self, old_items: Sequence[ItemId]) -> ItemList:
"""Given a list of ids from the other state, return the corresponding """Given a list of ids from the other state, return the corresponding ids for this state."""
ids for this state."""
class CardState(ItemState): class CardState(ItemState):
@ -705,9 +703,6 @@ class CardState(ItemState):
def toggle_state(self) -> NoteState: def toggle_state(self) -> NoteState:
return NoteState(self.col) return NoteState(self.col)
def get_new_item(self, old_item: ItemId) -> CardId:
return super().card_ids_from_note_ids([old_item])[0]
def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[CardId]: def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[CardId]:
return super().card_ids_from_note_ids(old_items) return super().card_ids_from_note_ids(old_items)
@ -725,11 +720,13 @@ class NoteState(ItemState):
def _load_columns(self) -> None: def _load_columns(self) -> None:
self._columns = [ self._columns = [
("note", tr.browsing_note()), ("note", tr.browsing_note()),
("noteCards", tr.qt_accel_cards().replace("&", "")), ("noteCards", tr.editing_cards()),
("noteCrt", tr.browsing_created()), ("noteCrt", tr.browsing_created()),
("noteEase", tr.browsing_average_ease()), ("noteEase", tr.browsing_average_ease()),
("noteFld", tr.browsing_sort_field()), ("noteFld", tr.browsing_sort_field()),
("noteLapses", tr.scheduling_lapses()),
("noteMod", tr.search_note_modified()), ("noteMod", tr.search_note_modified()),
("noteReps", tr.scheduling_reviews()),
("noteTags", tr.editing_tags()), ("noteTags", tr.editing_tags()),
] ]
self._columns.sort(key=itemgetter(1)) self._columns.sort(key=itemgetter(1))
@ -793,9 +790,6 @@ class NoteState(ItemState):
def toggle_state(self) -> CardState: def toggle_state(self) -> CardState:
return CardState(self.col) return CardState(self.col)
def get_new_item(self, old_item: ItemId) -> NoteId:
return super().note_ids_from_card_ids([old_item])[0]
def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[NoteId]: def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[NoteId]:
return super().note_ids_from_card_ids(old_items) return super().note_ids_from_card_ids(old_items)
@ -811,6 +805,8 @@ class Cell:
class CellRow: class CellRow:
is_deleted: bool = False
def __init__( def __init__(
self, self,
cells: Generator[Tuple[str, bool], None, None], cells: Generator[Tuple[str, bool], None, None],
@ -842,7 +838,9 @@ class CellRow:
@staticmethod @staticmethod
def deleted(length: int) -> CellRow: def deleted(length: int) -> CellRow:
return CellRow.generic(length, tr.browsing_row_deleted()) row = CellRow.generic(length, tr.browsing_row_deleted())
row.is_deleted = True
return row
def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]: def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]:
@ -896,7 +894,7 @@ class DataModel(QAbstractTableModel):
self._rows[item] = self._fetch_row_from_backend(item) self._rows[item] = self._fetch_row_from_backend(item)
return self._rows[item] return self._rows[item]
def _fetch_row_from_backend(self, item: int) -> CellRow: def _fetch_row_from_backend(self, item: ItemId) -> CellRow:
try: try:
row = CellRow(*self.col.browser_row_for_id(item)) row = CellRow(*self.col.browser_row_for_id(item))
except NotFoundError: except NotFoundError:
@ -904,8 +902,9 @@ class DataModel(QAbstractTableModel):
except Exception as e: except Exception as e:
return CellRow.generic(self.len_columns(), str(e)) return CellRow.generic(self.len_columns(), str(e))
# fixme: hook needs state gui_hooks.browser_did_fetch_row(
gui_hooks.browser_did_fetch_row(item, row, self._state.active_columns) item, self._state.is_notes_mode(), row, self._state.active_columns
)
return row return row
# Reset # Reset
@ -1113,6 +1112,8 @@ class DataModel(QAbstractTableModel):
return None return None
def flags(self, index: QModelIndex) -> Qt.ItemFlags: def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if self.get_row(index).is_deleted:
return Qt.ItemFlags(Qt.NoItemFlags)
return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable) return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable)

View file

@ -406,7 +406,12 @@ hooks = [
), ),
Hook( Hook(
name="browser_did_fetch_row", name="browser_did_fetch_row",
args=["card_id: int", "row: aqt.table.CellRow", "columns: Sequence[str]"], args=[
"card_or_note_id: aqt.table.ItemId",
"is_note: bool",
"row: aqt.table.CellRow",
"columns: Sequence[str]",
],
doc="""Allows you to add or modify content to a row in the browser. doc="""Allows you to add or modify content to a row in the browser.
You can mutate the row object to change what is displayed. Any columns the You can mutate the row object to change what is displayed. Any columns the

View file

@ -810,18 +810,20 @@ message SortOrder {
NOTE_CARDS = 0; NOTE_CARDS = 0;
NOTE_CREATION = 1; NOTE_CREATION = 1;
NOTE_EASE = 2; NOTE_EASE = 2;
NOTE_MOD = 3; NOTE_FIELD = 3;
NOTE_FIELD = 4; NOTE_LAPSES = 4;
NOTE_TAGS = 5; NOTE_MOD = 5;
NOTETYPE = 6; NOTE_REPS = 6;
CARD_MOD = 7; NOTE_TAGS = 7;
CARD_REPS = 8; NOTETYPE = 8;
CARD_DUE = 9; CARD_MOD = 9;
CARD_EASE = 10; CARD_REPS = 10;
CARD_LAPSES = 11; CARD_DUE = 11;
CARD_INTERVAL = 12; CARD_EASE = 12;
CARD_DECK = 13; CARD_LAPSES = 13;
CARD_TEMPLATE = 14; CARD_INTERVAL = 14;
CARD_DECK = 15;
CARD_TEMPLATE = 16;
} }
Kind kind = 1; Kind kind = 1;
bool reverse = 2; bool reverse = 2;

View file

@ -100,8 +100,10 @@ impl From<SortKindProto> for SortKind {
SortKindProto::NoteCards => SortKind::NoteCards, SortKindProto::NoteCards => SortKind::NoteCards,
SortKindProto::NoteCreation => SortKind::NoteCreation, SortKindProto::NoteCreation => SortKind::NoteCreation,
SortKindProto::NoteEase => SortKind::NoteEase, SortKindProto::NoteEase => SortKind::NoteEase,
SortKindProto::NoteLapses => SortKind::NoteLapses,
SortKindProto::NoteMod => SortKind::NoteMod, SortKindProto::NoteMod => SortKind::NoteMod,
SortKindProto::NoteField => SortKind::NoteField, SortKindProto::NoteField => SortKind::NoteField,
SortKindProto::NoteReps => SortKind::NoteReps,
SortKindProto::NoteTags => SortKind::NoteTags, SortKindProto::NoteTags => SortKind::NoteTags,
SortKindProto::Notetype => SortKind::Notetype, SortKindProto::Notetype => SortKind::Notetype,
SortKindProto::CardMod => SortKind::CardMod, SortKindProto::CardMod => SortKind::CardMod,

View file

@ -428,7 +428,9 @@ impl RowContext for NoteRowContext<'_> {
"noteCrt" => self.note_creation_str(), "noteCrt" => self.note_creation_str(),
"noteEase" => self.note_ease_str(), "noteEase" => self.note_ease_str(),
"noteFld" => self.note_field_str(), "noteFld" => self.note_field_str(),
"noteLapses" => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
"noteMod" => self.note.mtime.date_string(), "noteMod" => self.note.mtime.date_string(),
"noteReps" => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
"noteTags" => self.note.tags.join(" "), "noteTags" => self.note.tags.join(" "),
_ => "".to_string(), _ => "".to_string(),
}) })

View file

@ -275,9 +275,11 @@ pub enum SortKind {
#[serde(rename = "noteCrt")] #[serde(rename = "noteCrt")]
NoteCreation, NoteCreation,
NoteEase, NoteEase,
NoteLapses,
NoteMod, NoteMod,
#[serde(rename = "noteFld")] #[serde(rename = "noteFld")]
NoteField, NoteField,
NoteReps,
#[serde(rename = "note")] #[serde(rename = "note")]
Notetype, Notetype,
NoteTags, NoteTags,

View file

@ -91,10 +91,12 @@ impl SortKind {
SortKind::NoteCards SortKind::NoteCards
| SortKind::NoteCreation | SortKind::NoteCreation
| SortKind::NoteEase | SortKind::NoteEase
| SortKind::NoteMod
| SortKind::NoteField | SortKind::NoteField
| SortKind::Notetype | SortKind::NoteLapses
| SortKind::NoteTags => RequiredTable::Notes, | SortKind::NoteMod
| SortKind::NoteReps
| SortKind::NoteTags
| SortKind::Notetype => RequiredTable::Notes,
SortKind::CardTemplate => RequiredTable::CardsAndNotes, SortKind::CardTemplate => RequiredTable::CardsAndNotes,
SortKind::CardMod SortKind::CardMod
| SortKind::CardReps | SortKind::CardReps
@ -250,11 +252,12 @@ fn card_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> { fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
match kind { match kind {
SortKind::NoteCards => "(select pos from sort_order where nid = n.id) asc".into(), SortKind::NoteCards | SortKind::NoteEase | SortKind::NoteLapses | SortKind::NoteReps => {
"(select pos from sort_order where nid = n.id) asc".into()
}
SortKind::NoteCreation => "n.id asc".into(), SortKind::NoteCreation => "n.id asc".into(),
SortKind::NoteEase => "(select pos from sort_order where nid = n.id) asc".into(),
SortKind::NoteMod => "n.mod asc".into(),
SortKind::NoteField => "n.sfld collate nocase asc".into(), SortKind::NoteField => "n.sfld collate nocase asc".into(),
SortKind::NoteMod => "n.mod asc".into(),
SortKind::NoteTags => "n.tags asc".into(), SortKind::NoteTags => "n.tags asc".into(),
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(), SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
_ => "".into(), _ => "".into(),
@ -265,10 +268,12 @@ fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
use SortKind::*; use SortKind::*;
let sql = match kind { let sql = match kind {
CardDeck => include_str!("deck_order.sql"), CardDeck => include_str!("deck_order.sql"),
Notetype => include_str!("notetype_order.sql"),
CardTemplate => include_str!("template_order.sql"), CardTemplate => include_str!("template_order.sql"),
NoteCards => include_str!("note_cards_order.sql"), NoteCards => include_str!("note_cards_order.sql"),
NoteEase => include_str!("note_ease_order.sql"), NoteEase => include_str!("note_ease_order.sql"),
NoteLapses => include_str!("note_lapses_order.sql"),
NoteReps => include_str!("note_reps_order.sql"),
Notetype => include_str!("notetype_order.sql"),
_ => return Ok(()), _ => return Ok(()),
}; };

View 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 SUM(lapses);

View 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 SUM(reps);