mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
Merge branch 'ankitects:main' into main
This commit is contained in:
commit
12e69cb668
39 changed files with 623 additions and 372 deletions
|
@ -23,10 +23,13 @@ deck-config-new-limit-tooltip =
|
||||||
deck-config-review-limit-tooltip =
|
deck-config-review-limit-tooltip =
|
||||||
The maximum number of review cards to show in a day,
|
The maximum number of review cards to show in a day,
|
||||||
if cards are ready for review.
|
if cards are ready for review.
|
||||||
|
deck-config-limit-deck-v3 =
|
||||||
|
When studying a deck that has subdecks inside it, the limits set on each
|
||||||
|
subdeck control the maximum number of cards drawn from that particular deck.
|
||||||
|
The selected deck's limits control the total cards that will be shown.
|
||||||
|
|
||||||
## Learning section
|
## New Cards section
|
||||||
|
|
||||||
deck-config-learning-title = Learning
|
|
||||||
deck-config-learning-steps = Learning steps
|
deck-config-learning-steps = Learning steps
|
||||||
# Please don't translate `5m` or `2d`
|
# Please don't translate `5m` or `2d`
|
||||||
-deck-config-delay-hint = Delays can be in minutes (eg `5m`), or days (eg `2d`).
|
-deck-config-delay-hint = Delays can be in minutes (eg `5m`), or days (eg `2d`).
|
||||||
|
@ -36,14 +39,6 @@ deck-config-learning-steps-tooltip =
|
||||||
The `Good` button will advance to the next step, which is 10 minutes by default.
|
The `Good` button will advance to the next step, which is 10 minutes by default.
|
||||||
Once all steps have been passed, the card will become a review card, and
|
Once all steps have been passed, the card will become a review card, and
|
||||||
will appear on a different day. { -deck-config-delay-hint }
|
will appear on a different day. { -deck-config-delay-hint }
|
||||||
deck-config-interday-step-priority = Interday step priority
|
|
||||||
deck-config-interday-step-priority-tooltip = When to show (re)learning cards that cross a day boundary.
|
|
||||||
deck-config-review-mix-mix-with-reviews = Mix with reviews
|
|
||||||
deck-config-review-mix-show-after-reviews = Show after reviews
|
|
||||||
deck-config-review-mix-show-before-reviews = Show before reviews
|
|
||||||
|
|
||||||
## New Cards section
|
|
||||||
|
|
||||||
deck-config-graduating-interval-tooltip =
|
deck-config-graduating-interval-tooltip =
|
||||||
The number of days to wait before showing a card again, after the `Good` button
|
The number of days to wait before showing a card again, after the `Good` button
|
||||||
is pressed on the final learning step.
|
is pressed on the final learning step.
|
||||||
|
@ -53,48 +48,10 @@ deck-config-easy-interval-tooltip =
|
||||||
deck-config-new-insertion-order = Insertion order
|
deck-config-new-insertion-order = Insertion order
|
||||||
deck-config-new-insertion-order-tooltip =
|
deck-config-new-insertion-order-tooltip =
|
||||||
Controls the position (due #) new cards are assigned when you add new cards.
|
Controls the position (due #) new cards are assigned when you add new cards.
|
||||||
Cards with a lower due # will be shown first when studying. Changing this
|
Cards with a lower due number will be shown first when studying. Changing
|
||||||
option will automatically update the existing position of new cards.
|
this option will automatically update the existing position of new cards.
|
||||||
deck-config-new-insertion-order-sequential = Sequential (oldest cards first)
|
deck-config-new-insertion-order-sequential = Sequential (oldest cards first)
|
||||||
deck-config-new-insertion-order-random = Random
|
deck-config-new-insertion-order-random = Random
|
||||||
deck-config-new-gather-priority = Gather priority
|
|
||||||
deck-config-new-gather-priority-tooltip =
|
|
||||||
`Deck`: gathers cards from each subdeck in order, and stops when the
|
|
||||||
limit of the selected deck has been exceeded. This is faster, and allows you
|
|
||||||
to prioritize subdecks that are closer to the top.
|
|
||||||
|
|
||||||
`Position`: gathers cards from all decks before they are sorted. This
|
|
||||||
ensures the oldest cards will be shown first, even if the parent limit is
|
|
||||||
not high enough to see cards from all decks.
|
|
||||||
deck-config-new-gather-priority-deck = Deck
|
|
||||||
deck-config-new-gather-priority-position = Position
|
|
||||||
deck-config-sort-order = Sort order
|
|
||||||
deck-config-sort-order-tooltip =
|
|
||||||
This option controls how cards are sorted after they have been gathered.
|
|
||||||
By default, Anki sorts by card template first, to avoid multiple cards of
|
|
||||||
the same note from being shown in succession.
|
|
||||||
deck-config-sort-order-card-template-then-position = Card template, then position
|
|
||||||
deck-config-sort-order-card-template-then-random = Card template, then random
|
|
||||||
deck-config-sort-order-position = Position (siblings together)
|
|
||||||
deck-config-sort-order-random = Random
|
|
||||||
deck-config-review-priority = Review priority
|
|
||||||
deck-config-review-priority-tooltip = When to show these cards in relation to review cards.
|
|
||||||
|
|
||||||
## Review section
|
|
||||||
|
|
||||||
deck-config-review-sort-order-tooltip =
|
|
||||||
The default order fetches cards from each subdeck in turn, stopping when the limit
|
|
||||||
of the selected deck has been reached. The gathered cards are then shuffled together,
|
|
||||||
and shown in due date order. Because gathering stops when the parent limit has been
|
|
||||||
reached, your child decks should have smaller limits if you wish to see cards from
|
|
||||||
multiple decks at once.
|
|
||||||
|
|
||||||
The other sort options are mainly useful when catching up from a large backlog.
|
|
||||||
Because they have to sort all the cards first, they can be considerably slower
|
|
||||||
than the default sort order when many cards are due.
|
|
||||||
deck-config-sort-order-due-date-then-random = Due date, then random
|
|
||||||
deck-config-sort-order-ascending-intervals = Ascending intervals
|
|
||||||
deck-config-sort-order-descending-intervals = Descending intervals
|
|
||||||
|
|
||||||
## Lapses section
|
## Lapses section
|
||||||
|
|
||||||
|
@ -118,12 +75,53 @@ deck-config-leech-action-tooltip =
|
||||||
|
|
||||||
## Burying section
|
## Burying section
|
||||||
|
|
||||||
|
deck-config-bury-title = Burying
|
||||||
deck-config-bury-new-siblings = Bury new siblings until the next day
|
deck-config-bury-new-siblings = Bury new siblings until the next day
|
||||||
deck-config-bury-review-siblings = Bury review siblings until the next day
|
deck-config-bury-review-siblings = Bury review siblings until the next day
|
||||||
deck-config-bury-tooltip =
|
deck-config-bury-tooltip =
|
||||||
Whether other cards of the same note (eg reverse cards, adjacent
|
Whether other cards of the same note (eg reverse cards, adjacent
|
||||||
cloze deletions) will be delayed until the next day.
|
cloze deletions) will be delayed until the next day.
|
||||||
|
|
||||||
|
## Ordering section
|
||||||
|
|
||||||
|
deck-config-ordering-title = Display Order
|
||||||
|
deck-config-new-gather-priority = New card gather priority
|
||||||
|
deck-config-new-gather-priority-tooltip =
|
||||||
|
`Deck`: gathers cards from each subdeck in order, and stops when the
|
||||||
|
limit of the selected deck has been exceeded. This is faster, and allows you
|
||||||
|
to prioritize subdecks that are closer to the top.
|
||||||
|
|
||||||
|
`Position`: gathers cards from all decks before they are sorted. This
|
||||||
|
ensures the oldest cards will be shown first, even if the parent limit is
|
||||||
|
not high enough to see cards from all decks.
|
||||||
|
deck-config-new-gather-priority-deck = Deck
|
||||||
|
deck-config-new-gather-priority-position = Position
|
||||||
|
deck-config-new-card-sort-order = New card sort order
|
||||||
|
deck-config-new-card-sort-order-tooltip =
|
||||||
|
How cards are sorted after they have been gathered. By default, Anki sorts
|
||||||
|
by card template first, to avoid multiple cards of the same note from being
|
||||||
|
shown in succession.
|
||||||
|
deck-config-sort-order-card-template-then-position = Card template, then position
|
||||||
|
deck-config-sort-order-card-template-then-random = Card template, then random
|
||||||
|
deck-config-sort-order-position = Position (siblings together)
|
||||||
|
deck-config-sort-order-random = Random
|
||||||
|
deck-config-new-review-priority = New/review priority
|
||||||
|
deck-config-new-review-priority-tooltip = When to show new cards in relation to review cards.
|
||||||
|
deck-config-interday-step-priority = Interday learning/review priority
|
||||||
|
deck-config-interday-step-priority-tooltip = When to show (re)learning cards that cross a day boundary.
|
||||||
|
deck-config-review-mix-mix-with-reviews = Mix with reviews
|
||||||
|
deck-config-review-mix-show-after-reviews = Show after reviews
|
||||||
|
deck-config-review-mix-show-before-reviews = Show before reviews
|
||||||
|
deck-config-review-sort-order = Review sort order
|
||||||
|
deck-config-review-sort-order-tooltip =
|
||||||
|
The default order prioritizes cards that have been waiting longest, so that
|
||||||
|
if you have a backlog of reviews, the longest-waiting ones will appear
|
||||||
|
first. If you have a large backlog that will take more than a few days to
|
||||||
|
clear, you may find the alternate sort orders preferable.
|
||||||
|
deck-config-sort-order-due-date-then-random = Due date, then random
|
||||||
|
deck-config-sort-order-ascending-intervals = Ascending intervals
|
||||||
|
deck-config-sort-order-descending-intervals = Descending intervals
|
||||||
|
|
||||||
## Timer section
|
## Timer section
|
||||||
|
|
||||||
deck-config-timer-title = Timer
|
deck-config-timer-title = Timer
|
||||||
|
|
|
@ -789,8 +789,17 @@ class Collection:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def set_config(self, key: str, val: Any) -> OpChanges:
|
def set_config(self, key: str, val: Any, *, undoable: bool = False) -> OpChanges:
|
||||||
return self._backend.set_config_json(key=key, value_json=to_json_bytes(val))
|
"""Set a single config variable to any JSON-serializable value. The config
|
||||||
|
is currently sent on every sync, so please don't store more than a few
|
||||||
|
kilobytes in it.
|
||||||
|
|
||||||
|
By default, no undo entry will be created, but the existing undo history
|
||||||
|
will be preserved. Set `undoable=True` to allow the change to be undone;
|
||||||
|
see undo code for how you can merge multiple undo entries."""
|
||||||
|
return self._backend.set_config_json(
|
||||||
|
key=key, value_json=to_json_bytes(val), undoable=undoable
|
||||||
|
)
|
||||||
|
|
||||||
def remove_config(self, key: str) -> OpChanges:
|
def remove_config(self, key: str) -> OpChanges:
|
||||||
return self.conf.remove(key)
|
return self.conf.remove(key)
|
||||||
|
@ -802,14 +811,18 @@ class Collection:
|
||||||
def get_config_bool(self, key: Config.Bool.Key.V) -> bool:
|
def get_config_bool(self, key: Config.Bool.Key.V) -> bool:
|
||||||
return self._backend.get_config_bool(key)
|
return self._backend.get_config_bool(key)
|
||||||
|
|
||||||
def set_config_bool(self, key: Config.Bool.Key.V, value: bool) -> OpChanges:
|
def set_config_bool(
|
||||||
return self._backend.set_config_bool(key=key, value=value)
|
self, key: Config.Bool.Key.V, value: bool, *, undoable: bool = False
|
||||||
|
) -> OpChanges:
|
||||||
|
return self._backend.set_config_bool(key=key, value=value, undoable=undoable)
|
||||||
|
|
||||||
def get_config_string(self, key: Config.String.Key.V) -> str:
|
def get_config_string(self, key: Config.String.Key.V) -> str:
|
||||||
return self._backend.get_config_string(key)
|
return self._backend.get_config_string(key)
|
||||||
|
|
||||||
def set_config_string(self, key: Config.String.Key.V, value: str) -> OpChanges:
|
def set_config_string(
|
||||||
return self._backend.set_config_string(key=key, value=value)
|
self, key: Config.String.Key.V, value: str, undoable: bool = False
|
||||||
|
) -> OpChanges:
|
||||||
|
return self._backend.set_config_string(key=key, value=value, undoable=undoable)
|
||||||
|
|
||||||
# Stats
|
# Stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -45,7 +45,10 @@ class ConfigManager:
|
||||||
|
|
||||||
def set(self, key: str, val: Any) -> None:
|
def set(self, key: str, val: Any) -> None:
|
||||||
self.col._backend.set_config_json_no_undo(
|
self.col._backend.set_config_json_no_undo(
|
||||||
key=key, value_json=to_json_bytes(val)
|
key=key,
|
||||||
|
value_json=to_json_bytes(val),
|
||||||
|
# this argument is ignored
|
||||||
|
undoable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove(self, key: str) -> OpChanges:
|
def remove(self, key: str) -> OpChanges:
|
||||||
|
|
|
@ -170,7 +170,8 @@ class ModelManager:
|
||||||
return self.get(NotetypeId(self.all_names_and_ids()[0].id))
|
return self.get(NotetypeId(self.all_names_and_ids()[0].id))
|
||||||
|
|
||||||
def setCurrent(self, m: NotetypeDict) -> None:
|
def setCurrent(self, m: NotetypeDict) -> None:
|
||||||
self.col.conf["curModel"] = m["id"]
|
"""Legacy. The current notetype is now updated on note add."""
|
||||||
|
self.col.set_config("curModel", m["id"])
|
||||||
|
|
||||||
# Retrieving and creating models
|
# Retrieving and creating models
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, List, Tuple
|
from typing import Any, Callable, List, Tuple
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
@ -40,11 +40,11 @@ def get_stock_notetypes(
|
||||||
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
|
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
|
||||||
|
|
||||||
def instance_getter(
|
def instance_getter(
|
||||||
col: anki.collection.Collection,
|
model: Any,
|
||||||
) -> anki.models.NotetypeDict:
|
) -> Callable[[anki.collection.Collection], anki.models.NotetypeDict]:
|
||||||
return m # pylint:disable=cell-var-from-loop
|
return lambda col: model
|
||||||
|
|
||||||
out.append((m["name"], instance_getter))
|
out.append((m["name"], instance_getter(m)))
|
||||||
# add extras from add-ons
|
# add extras from add-ons
|
||||||
for (name_or_func, func) in models:
|
for (name_or_func, func) in models:
|
||||||
if not isinstance(name_or_func, str):
|
if not isinstance(name_or_func, str):
|
||||||
|
|
|
@ -132,6 +132,7 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
if changes.browser_table and changes.card:
|
if changes.browser_table and changes.card:
|
||||||
self.card = self.table.get_current_card()
|
self.card = self.table.get_current_card()
|
||||||
|
self._update_context_actions()
|
||||||
|
|
||||||
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
||||||
if current_top_level_widget() == self:
|
if current_top_level_widget() == self:
|
||||||
|
@ -498,21 +499,6 @@ class Browser(QMainWindow):
|
||||||
def selectedNotesAsCards(self) -> Sequence[CardId]:
|
def selectedNotesAsCards(self) -> Sequence[CardId]:
|
||||||
return self.table.get_card_ids_from_selected_note_ids()
|
return self.table.get_card_ids_from_selected_note_ids()
|
||||||
|
|
||||||
def oneModelNotes(self) -> Sequence[NoteId]:
|
|
||||||
sf = self.selected_notes()
|
|
||||||
if not sf:
|
|
||||||
return []
|
|
||||||
mods = self.col.db.scalar(
|
|
||||||
"""
|
|
||||||
select count(distinct mid) from notes
|
|
||||||
where id in %s"""
|
|
||||||
% ids2str(sf)
|
|
||||||
)
|
|
||||||
if mods > 1:
|
|
||||||
showInfo(tr.browsing_please_select_cards_from_only_one())
|
|
||||||
return []
|
|
||||||
return sf
|
|
||||||
|
|
||||||
def onHelp(self) -> None:
|
def onHelp(self) -> None:
|
||||||
openHelp(HelpPage.BROWSING)
|
openHelp(HelpPage.BROWSING)
|
||||||
|
|
||||||
|
@ -528,7 +514,17 @@ where id in %s"""
|
||||||
@skip_if_selection_is_empty
|
@skip_if_selection_is_empty
|
||||||
@ensure_editor_saved
|
@ensure_editor_saved
|
||||||
def onChangeModel(self) -> None:
|
def onChangeModel(self) -> None:
|
||||||
ChangeModel(self, self.oneModelNotes())
|
ids = self.selected_notes()
|
||||||
|
if self._is_one_notetype(ids):
|
||||||
|
ChangeModel(self, ids)
|
||||||
|
else:
|
||||||
|
showInfo(tr.browsing_please_select_cards_from_only_one())
|
||||||
|
|
||||||
|
def _is_one_notetype(self, ids: Sequence[NoteId]) -> bool:
|
||||||
|
query = f"select count(distinct mid) from notes where id in {ids2str(ids)}"
|
||||||
|
if self.col.db.scalar(query) == 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def createFilteredDeck(self) -> None:
|
def createFilteredDeck(self) -> None:
|
||||||
search = self.current_search()
|
search = self.current_search()
|
||||||
|
|
|
@ -40,12 +40,11 @@ class SidebarToolbar(QToolBar):
|
||||||
action.setCheckable(True)
|
action.setCheckable(True)
|
||||||
action.setShortcut(f"Alt+{row + 1}")
|
action.setShortcut(f"Alt+{row + 1}")
|
||||||
self._action_group.addAction(action)
|
self._action_group.addAction(action)
|
||||||
saved = self.sidebar.col.get_config("sidebarTool", 0)
|
# always start with first tool
|
||||||
active = saved if saved < len(self._tools) else 0
|
active = 0
|
||||||
self._action_group.actions()[active].setChecked(True)
|
self._action_group.actions()[active].setChecked(True)
|
||||||
self.sidebar.tool = self._tools[active][0]
|
self.sidebar.tool = self._tools[active][0]
|
||||||
|
|
||||||
def _on_action_group_triggered(self, action: QAction) -> None:
|
def _on_action_group_triggered(self, action: QAction) -> None:
|
||||||
index = self._action_group.actions().index(action)
|
index = self._action_group.actions().index(action)
|
||||||
self.sidebar.col.set_config("sidebarTool", index)
|
|
||||||
self.sidebar.tool = self._tools[index][0]
|
self.sidebar.tool = self._tools[index][0]
|
||||||
|
|
|
@ -180,7 +180,8 @@ class Table:
|
||||||
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, self.is_notes_mode()
|
Config.Bool.BROWSER_TABLE_SHOW_NOTES_MODE,
|
||||||
|
self.is_notes_mode(),
|
||||||
)
|
)
|
||||||
self._restore_header()
|
self._restore_header()
|
||||||
self._restore_selection(self._toggled_selection)
|
self._restore_selection(self._toggled_selection)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from aqt import colors
|
from aqt import colors
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -21,6 +22,8 @@ class Switch(QAbstractButton):
|
||||||
radius: int = 10,
|
radius: int = 10,
|
||||||
left_label: str = "",
|
left_label: str = "",
|
||||||
right_label: str = "",
|
right_label: str = "",
|
||||||
|
left_color: Tuple[str, str] = colors.FLAG4_BG,
|
||||||
|
right_color: Tuple[str, str] = colors.FLAG3_BG,
|
||||||
parent: QWidget = None,
|
parent: QWidget = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
@ -29,9 +32,10 @@ class Switch(QAbstractButton):
|
||||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
self._left_label = left_label
|
self._left_label = left_label
|
||||||
self._right_label = right_label
|
self._right_label = right_label
|
||||||
self._path_radius = radius
|
self._left_color = left_color
|
||||||
self._knob_radius = radius - self._margin
|
self._right_color = right_color
|
||||||
self._left_position = self._position = self._path_radius + self._margin
|
self._path_radius = radius - self._margin
|
||||||
|
self._knob_radius = self._left_position = self._position = radius
|
||||||
self._right_position = 3 * self._path_radius + self._margin
|
self._right_position = 3 * self._path_radius + self._margin
|
||||||
|
|
||||||
@pyqtProperty(int) # type: ignore
|
@pyqtProperty(int) # type: ignore
|
||||||
|
@ -55,6 +59,11 @@ class Switch(QAbstractButton):
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return self._right_label if self.isChecked() else self._left_label
|
return self._right_label if self.isChecked() else self._left_label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_color(self) -> QColor:
|
||||||
|
color = self._right_color if self.isChecked() else self._left_color
|
||||||
|
return theme_manager.qcolor(color)
|
||||||
|
|
||||||
def sizeHint(self) -> QSize:
|
def sizeHint(self) -> QSize:
|
||||||
return QSize(
|
return QSize(
|
||||||
4 * self._path_radius + 2 * self._margin,
|
4 * self._path_radius + 2 * self._margin,
|
||||||
|
@ -75,7 +84,7 @@ class Switch(QAbstractButton):
|
||||||
self._paint_label(painter)
|
self._paint_label(painter)
|
||||||
|
|
||||||
def _paint_path(self, painter: QPainter) -> None:
|
def _paint_path(self, painter: QPainter) -> None:
|
||||||
painter.setBrush(QBrush(theme_manager.qcolor(colors.FRAME_BG)))
|
painter.setBrush(QBrush(self.path_color))
|
||||||
rectangle = QRectF(
|
rectangle = QRectF(
|
||||||
self._margin,
|
self._margin,
|
||||||
self._margin,
|
self._margin,
|
||||||
|
@ -87,19 +96,23 @@ class Switch(QAbstractButton):
|
||||||
def _current_knob_rectangle(self) -> QRectF:
|
def _current_knob_rectangle(self) -> QRectF:
|
||||||
return QRectF(
|
return QRectF(
|
||||||
self.position - self._knob_radius, # type: ignore
|
self.position - self._knob_radius, # type: ignore
|
||||||
2 * self._margin,
|
0,
|
||||||
2 * self._knob_radius,
|
2 * self._knob_radius,
|
||||||
2 * self._knob_radius,
|
2 * self._knob_radius,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _paint_knob(self, painter: QPainter) -> None:
|
def _paint_knob(self, painter: QPainter) -> None:
|
||||||
painter.setBrush(QBrush(theme_manager.qcolor(colors.LINK)))
|
if theme_manager.night_mode:
|
||||||
|
color = QColor(theme_manager.DARK_MODE_BUTTON_BG_MIDPOINT)
|
||||||
|
else:
|
||||||
|
color = theme_manager.qcolor(colors.FRAME_BG)
|
||||||
|
painter.setBrush(QBrush(color))
|
||||||
painter.drawEllipse(self._current_knob_rectangle())
|
painter.drawEllipse(self._current_knob_rectangle())
|
||||||
|
|
||||||
def _paint_label(self, painter: QPainter) -> None:
|
def _paint_label(self, painter: QPainter) -> None:
|
||||||
painter.setPen(QColor("white"))
|
painter.setPen(theme_manager.qcolor(colors.SLIGHTLY_GREY_TEXT))
|
||||||
font = painter.font()
|
font = painter.font()
|
||||||
font.setPixelSize(int(1.5 * self._knob_radius))
|
font.setPixelSize(int(1.2 * self._knob_radius))
|
||||||
painter.setFont(font)
|
painter.setFont(font)
|
||||||
painter.drawText(self._current_knob_rectangle(), Qt.AlignCenter, self.label)
|
painter.drawText(self._current_knob_rectangle(), Qt.AlignCenter, self.label)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ class ThemeManager:
|
||||||
_icon_size = 128
|
_icon_size = 128
|
||||||
_dark_mode_available: Optional[bool] = None
|
_dark_mode_available: Optional[bool] = None
|
||||||
|
|
||||||
|
# Qt applies a gradient to the buttons in dark mode
|
||||||
|
# from about #505050 to #606060.
|
||||||
|
DARK_MODE_BUTTON_BG_MIDPOINT = "#555555"
|
||||||
|
|
||||||
def macos_dark_mode(self) -> bool:
|
def macos_dark_mode(self) -> bool:
|
||||||
"True if the user has night mode on, and has forced native widgets."
|
"True if the user has night mode on, and has forced native widgets."
|
||||||
if not isMac:
|
if not isMac:
|
||||||
|
|
|
@ -988,6 +988,7 @@ message RenameTagsIn {
|
||||||
message SetConfigJsonIn {
|
message SetConfigJsonIn {
|
||||||
string key = 1;
|
string key = 1;
|
||||||
bytes value_json = 2;
|
bytes value_json = 2;
|
||||||
|
bool undoable = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StockNotetype {
|
message StockNotetype {
|
||||||
|
@ -1441,11 +1442,13 @@ message Config {
|
||||||
message SetConfigBoolIn {
|
message SetConfigBoolIn {
|
||||||
Config.Bool.Key key = 1;
|
Config.Bool.Key key = 1;
|
||||||
bool value = 2;
|
bool value = 2;
|
||||||
|
bool undoable = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetConfigStringIn {
|
message SetConfigStringIn {
|
||||||
Config.String.Key key = 1;
|
Config.String.Key key = 1;
|
||||||
string value = 2;
|
string value = 2;
|
||||||
|
bool undoable = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderMarkdownIn {
|
message RenderMarkdownIn {
|
||||||
|
|
|
@ -65,7 +65,7 @@ impl ConfigService for Backend {
|
||||||
fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result<pb::OpChanges> {
|
fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result<pb::OpChanges> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let val: Value = serde_json::from_slice(&input.value_json)?;
|
let val: Value = serde_json::from_slice(&input.value_json)?;
|
||||||
col.set_config_json(input.key.as_str(), &val)
|
col.set_config_json(input.key.as_str(), &val, input.undoable)
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ impl ConfigService for Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result<pb::OpChanges> {
|
fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result<pb::OpChanges> {
|
||||||
self.with_col(|col| col.set_config_bool(input.key().into(), input.value))
|
self.with_col(|col| col.set_config_bool(input.key().into(), input.value, input.undoable))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ impl ConfigService for Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result<pb::OpChanges> {
|
fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result<pb::OpChanges> {
|
||||||
self.with_col(|col| col.set_config_string(input.key().into(), &input.value))
|
self.with_col(|col| col.set_config_string(input.key().into(), &input.value, input.undoable))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,9 +70,18 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_config_bool(&mut self, key: BoolKey, value: bool) -> Result<OpOutput<()>> {
|
pub fn set_config_bool(
|
||||||
|
&mut self,
|
||||||
|
key: BoolKey,
|
||||||
|
value: bool,
|
||||||
|
undoable: bool,
|
||||||
|
) -> Result<OpOutput<()>> {
|
||||||
self.transact(Op::UpdateConfig, |col| {
|
self.transact(Op::UpdateConfig, |col| {
|
||||||
col.set_config(key, &value).map(|_| ())
|
col.set_config(key, &value)?;
|
||||||
|
if !undoable {
|
||||||
|
col.clear_current_undo_step_changes();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,8 +70,19 @@ pub enum SchedulerVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn set_config_json<T: Serialize>(&mut self, key: &str, val: &T) -> Result<OpOutput<()>> {
|
pub fn set_config_json<T: Serialize>(
|
||||||
self.transact(Op::UpdateConfig, |col| col.set_config(key, val).map(|_| ()))
|
&mut self,
|
||||||
|
key: &str,
|
||||||
|
val: &T,
|
||||||
|
undoable: bool,
|
||||||
|
) -> Result<OpOutput<()>> {
|
||||||
|
self.transact(Op::UpdateConfig, |col| {
|
||||||
|
col.set_config(key, val)?;
|
||||||
|
if !undoable {
|
||||||
|
col.clear_current_undo_step_changes();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_config(&mut self, key: &str) -> Result<OpOutput<()>> {
|
pub fn remove_config(&mut self, key: &str) -> Result<OpOutput<()>> {
|
||||||
|
|
|
@ -23,9 +23,18 @@ impl Collection {
|
||||||
.unwrap_or_else(|| default.to_string())
|
.unwrap_or_else(|| default.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_config_string(&mut self, key: StringKey, val: &str) -> Result<OpOutput<()>> {
|
pub fn set_config_string(
|
||||||
|
&mut self,
|
||||||
|
key: StringKey,
|
||||||
|
val: &str,
|
||||||
|
undoable: bool,
|
||||||
|
) -> Result<OpOutput<()>> {
|
||||||
self.transact(Op::UpdateConfig, |col| {
|
self.transact(Op::UpdateConfig, |col| {
|
||||||
col.set_config_string_inner(key, val).map(|_| ())
|
col.set_config_string_inner(key, val)?;
|
||||||
|
if !undoable {
|
||||||
|
col.clear_current_undo_step_changes();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -370,40 +370,52 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test helpers
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
impl Collection {
|
pub mod test_helpers {
|
||||||
pub(crate) fn answer_again(&mut self) {
|
use super::*;
|
||||||
|
|
||||||
|
pub struct PostAnswerState {
|
||||||
|
pub card_id: CardId,
|
||||||
|
pub new_state: CardState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub(crate) fn answer_again(&mut self) -> PostAnswerState {
|
||||||
self.answer(|states| states.again, Rating::Again).unwrap()
|
self.answer(|states| states.again, Rating::Again).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn answer_hard(&mut self) {
|
pub(crate) fn answer_hard(&mut self) -> PostAnswerState {
|
||||||
self.answer(|states| states.hard, Rating::Hard).unwrap()
|
self.answer(|states| states.hard, Rating::Hard).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn answer_good(&mut self) {
|
pub(crate) fn answer_good(&mut self) -> PostAnswerState {
|
||||||
self.answer(|states| states.good, Rating::Good).unwrap()
|
self.answer(|states| states.good, Rating::Good).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn answer_easy(&mut self) {
|
pub(crate) fn answer_easy(&mut self) -> PostAnswerState {
|
||||||
self.answer(|states| states.easy, Rating::Easy).unwrap()
|
self.answer(|states| states.easy, Rating::Easy).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn answer<F>(&mut self, get_state: F, rating: Rating) -> Result<()>
|
fn answer<F>(&mut self, get_state: F, rating: Rating) -> Result<PostAnswerState>
|
||||||
where
|
where
|
||||||
F: FnOnce(&NextCardStates) -> CardState,
|
F: FnOnce(&NextCardStates) -> CardState,
|
||||||
{
|
{
|
||||||
let queued = self.next_card()?.unwrap();
|
let queued = self.next_card()?.unwrap();
|
||||||
|
let new_state = get_state(&queued.next_states);
|
||||||
self.answer_card(&CardAnswer {
|
self.answer_card(&CardAnswer {
|
||||||
card_id: queued.card.id,
|
card_id: queued.card.id,
|
||||||
current_state: queued.next_states.current,
|
current_state: queued.next_states.current,
|
||||||
new_state: get_state(&queued.next_states),
|
new_state,
|
||||||
rating,
|
rating,
|
||||||
answered_at: TimestampMillis::now(),
|
answered_at: TimestampMillis::now(),
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(PostAnswerState {
|
||||||
|
card_id: queued.card.id,
|
||||||
|
new_state,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,3 +428,131 @@ fn get_fuzz_seed(card: &Card) -> Option<u64> {
|
||||||
Some((card.id.0 as u64).wrapping_add(card.reps as u64))
|
Some((card.id.0 as u64).wrapping_add(card.reps as u64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::{card::CardType, collection::open_test_collection};
|
||||||
|
|
||||||
|
fn current_state(col: &mut Collection, card_id: CardId) -> CardState {
|
||||||
|
col.get_next_card_states(card_id).unwrap().current
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the 'current' state for a card matches the
|
||||||
|
// state we applied to it
|
||||||
|
#[test]
|
||||||
|
fn state_application() -> Result<()> {
|
||||||
|
let mut col = open_test_collection();
|
||||||
|
if col.timing_today()?.near_cutoff() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
|
let mut note = nt.new_note();
|
||||||
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
|
||||||
|
// new->learning
|
||||||
|
let post_answer = col.answer_again();
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Learn);
|
||||||
|
assert_eq!(card.remaining_steps, 2);
|
||||||
|
|
||||||
|
// learning step
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let post_answer = col.answer_good();
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Learn);
|
||||||
|
assert_eq!(card.remaining_steps, 1);
|
||||||
|
|
||||||
|
// graduation
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let mut post_answer = col.answer_good();
|
||||||
|
// compensate for shifting the due date
|
||||||
|
if let CardState::Normal(NormalState::Review(state)) = &mut post_answer.new_state {
|
||||||
|
state.elapsed_days = 1;
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Review);
|
||||||
|
assert_eq!(card.interval, 1);
|
||||||
|
assert_eq!(card.remaining_steps, 0);
|
||||||
|
|
||||||
|
// answering a review card again; easy boost
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let mut post_answer = col.answer_easy();
|
||||||
|
if let CardState::Normal(NormalState::Review(state)) = &mut post_answer.new_state {
|
||||||
|
state.elapsed_days = 4;
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Review);
|
||||||
|
assert_eq!(card.interval, 4);
|
||||||
|
assert_eq!(card.ease_factor, 2650);
|
||||||
|
|
||||||
|
// lapsing it
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let mut post_answer = col.answer_again();
|
||||||
|
if let CardState::Normal(NormalState::Relearning(state)) = &mut post_answer.new_state {
|
||||||
|
state.review.elapsed_days = 1;
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Learn);
|
||||||
|
assert_eq!(card.ctype, CardType::Relearn);
|
||||||
|
assert_eq!(card.interval, 1);
|
||||||
|
assert_eq!(card.ease_factor, 2450);
|
||||||
|
assert_eq!(card.lapses, 1);
|
||||||
|
|
||||||
|
// failed in relearning
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let mut post_answer = col.answer_again();
|
||||||
|
if let CardState::Normal(NormalState::Relearning(state)) = &mut post_answer.new_state {
|
||||||
|
state.review.elapsed_days = 1;
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Learn);
|
||||||
|
assert_eq!(card.lapses, 1);
|
||||||
|
|
||||||
|
// re-graduating
|
||||||
|
col.storage.db.execute_batch("update cards set due=0")?;
|
||||||
|
col.clear_study_queues();
|
||||||
|
let mut post_answer = col.answer_good();
|
||||||
|
if let CardState::Normal(NormalState::Review(state)) = &mut post_answer.new_state {
|
||||||
|
state.elapsed_days = 1;
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
post_answer.new_state,
|
||||||
|
current_state(&mut col, post_answer.card_id)
|
||||||
|
);
|
||||||
|
let card = col.storage.get_card(post_answer.card_id)?.unwrap();
|
||||||
|
assert_eq!(card.queue, CardQueue::Review);
|
||||||
|
assert_eq!(card.interval, 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ impl CardStateUpdater {
|
||||||
self.card.remaining_steps = next.learning.remaining_steps;
|
self.card.remaining_steps = next.learning.remaining_steps;
|
||||||
self.card.ctype = CardType::Relearn;
|
self.card.ctype = CardType::Relearn;
|
||||||
self.card.lapses = next.review.lapses;
|
self.card.lapses = next.review.lapses;
|
||||||
|
self.card.ease_factor = (next.review.ease_factor * 1000.0).round() as u16;
|
||||||
|
|
||||||
let interval = next
|
let interval = next
|
||||||
.interval_kind()
|
.interval_kind()
|
||||||
|
|
|
@ -19,6 +19,7 @@ impl CardStateUpdater {
|
||||||
self.card.due = (self.timing.days_elapsed + next.scheduled_days) as i32;
|
self.card.due = (self.timing.days_elapsed + next.scheduled_days) as i32;
|
||||||
self.card.ease_factor = (next.ease_factor * 1000.0).round() as u16;
|
self.card.ease_factor = (next.ease_factor * 1000.0).round() as u16;
|
||||||
self.card.lapses = next.lapses;
|
self.card.lapses = next.lapses;
|
||||||
|
self.card.remaining_steps = 0;
|
||||||
|
|
||||||
RevlogEntryPartial::maybe_new(
|
RevlogEntryPartial::maybe_new(
|
||||||
current,
|
current,
|
||||||
|
|
|
@ -200,6 +200,9 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn undo_counts() -> Result<()> {
|
fn undo_counts() -> Result<()> {
|
||||||
let mut col = open_test_collection();
|
let mut col = open_test_collection();
|
||||||
|
if col.timing_today()?.near_cutoff() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(col.counts(), [0, 0, 0]);
|
assert_eq!(col.counts(), [0, 0, 0]);
|
||||||
add_note(&mut col, true)?;
|
add_note(&mut col, true)?;
|
||||||
|
|
|
@ -197,6 +197,18 @@ mod test {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// test helper
|
||||||
|
impl SchedTimingToday {
|
||||||
|
/// Check if less than 25 minutes until the rollover
|
||||||
|
pub fn near_cutoff(&self) -> bool {
|
||||||
|
let near = TimestampSecs::now().adding_secs(60 * 25) > self.next_day_at;
|
||||||
|
if near {
|
||||||
|
println!("this would fail near the rollover time");
|
||||||
|
}
|
||||||
|
near
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// static timezone for tests
|
// static timezone for tests
|
||||||
const AEST_MINS_WEST: i32 = -600;
|
const AEST_MINS_WEST: i32 = -600;
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,12 @@ impl UndoManager {
|
||||||
self.end_step();
|
self.end_step();
|
||||||
self.counter
|
self.counter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_current_changes(&mut self) {
|
||||||
|
if let Some(op) = &mut self.current_step {
|
||||||
|
op.changes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
@ -255,6 +261,12 @@ impl Collection {
|
||||||
self.state.undo.save(item.into());
|
self.state.undo.save(item.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forget any recorded changes in the current transaction, allowing
|
||||||
|
/// a minor change like a config update to bypass undo.
|
||||||
|
pub(crate) fn clear_current_undo_step_changes(&mut self) {
|
||||||
|
self.state.undo.clear_current_changes()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn current_undo_op(&self) -> Option<&UndoableOp> {
|
pub(crate) fn current_undo_op(&self) -> Option<&UndoableOp> {
|
||||||
self.state.undo.current_op()
|
self.state.undo.current_op()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,10 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { Modifier } from "lib/shortcuts";
|
|
||||||
|
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
||||||
|
|
||||||
export let shortcut: string;
|
export let shortcut: string;
|
||||||
export let optionalModifiers: Modifier[] | undefined = [];
|
|
||||||
|
|
||||||
const shortcutLabel = getPlatformString(shortcut);
|
const shortcutLabel = getPlatformString(shortcut);
|
||||||
|
|
||||||
|
@ -17,14 +14,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
function createShortcut({ detail }: CustomEvent): void {
|
function createShortcut({ detail }: CustomEvent): void {
|
||||||
const mounted: HTMLButtonElement = detail.button;
|
const mounted: HTMLButtonElement = detail.button;
|
||||||
deregister = registerShortcut(
|
deregister = registerShortcut((event: KeyboardEvent) => {
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
mounted.dispatchEvent(new MouseEvent("click", event));
|
mounted.dispatchEvent(new MouseEvent("click", event));
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
}, shortcut);
|
||||||
shortcut,
|
|
||||||
optionalModifiers
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => deregister());
|
onDestroy(() => deregister());
|
||||||
|
|
27
ts/deckoptions/BuryOptions.svelte
Normal file
27
ts/deckoptions/BuryOptions.svelte
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as tr from "lib/i18n";
|
||||||
|
import CheckBox from "./CheckBox.svelte";
|
||||||
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
|
||||||
|
export let state: DeckOptionsState;
|
||||||
|
let config = state.currentConfig;
|
||||||
|
let defaults = state.defaults;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>{tr.deckConfigBuryTitle()}</h2>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label={tr.deckConfigBuryNewSiblings()}
|
||||||
|
tooltip={tr.deckConfigBuryTooltip()}
|
||||||
|
defaultValue={defaults.buryNew}
|
||||||
|
bind:value={$config.buryNew} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label={tr.deckConfigBuryReviewSiblings()}
|
||||||
|
tooltip={tr.deckConfigBuryTooltip()}
|
||||||
|
defaultValue={defaults.buryReviews}
|
||||||
|
bind:value={$config.buryReviews} />
|
|
@ -4,10 +4,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DailyLimits from "./DailyLimits.svelte";
|
import DailyLimits from "./DailyLimits.svelte";
|
||||||
import LearningOptions from "./LearningOptions.svelte";
|
import DisplayOrder from "./DisplayOrder.svelte";
|
||||||
import NewOptions from "./NewOptions.svelte";
|
import NewOptions from "./NewOptions.svelte";
|
||||||
import AdvancedOptions from "./AdvancedOptions.svelte";
|
import AdvancedOptions from "./AdvancedOptions.svelte";
|
||||||
import ReviewOptions from "./ReviewOptions.svelte";
|
import BuryOptions from "./BuryOptions.svelte";
|
||||||
import LapseOptions from "./LapseOptions.svelte";
|
import LapseOptions from "./LapseOptions.svelte";
|
||||||
import GeneralOptions from "./GeneralOptions.svelte";
|
import GeneralOptions from "./GeneralOptions.svelte";
|
||||||
import Addons from "./Addons.svelte";
|
import Addons from "./Addons.svelte";
|
||||||
|
@ -36,10 +36,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<div class="outer">
|
<div class="outer">
|
||||||
<DailyLimits {state} />
|
<DailyLimits {state} />
|
||||||
<LearningOptions {state} />
|
|
||||||
<NewOptions {state} />
|
<NewOptions {state} />
|
||||||
<ReviewOptions {state} />
|
|
||||||
<LapseOptions {state} />
|
<LapseOptions {state} />
|
||||||
|
<BuryOptions {state} />
|
||||||
|
{#if state.v3Scheduler}
|
||||||
|
<DisplayOrder {state} />
|
||||||
|
{/if}
|
||||||
<GeneralOptions {state} />
|
<GeneralOptions {state} />
|
||||||
<Addons {state} />
|
<Addons {state} />
|
||||||
<AdvancedOptions {state} />
|
<AdvancedOptions {state} />
|
||||||
|
|
|
@ -12,6 +12,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let defaults = state.defaults;
|
let defaults = state.defaults;
|
||||||
let parentLimits = state.parentLimits;
|
let parentLimits = state.parentLimits;
|
||||||
|
|
||||||
|
const v3Extra = state.v3Scheduler ? "\n\n" + tr.deckConfigLimitDeckV3() : "";
|
||||||
|
|
||||||
$: newCardsGreaterThanParent =
|
$: newCardsGreaterThanParent =
|
||||||
!state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
|
!state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
|
||||||
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
||||||
|
@ -30,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<SpinBox
|
<SpinBox
|
||||||
label={tr.schedulingNewCardsday()}
|
label={tr.schedulingNewCardsday()}
|
||||||
tooltip={tr.deckConfigNewLimitTooltip()}
|
tooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||||
min={0}
|
min={0}
|
||||||
warnings={[newCardsGreaterThanParent]}
|
warnings={[newCardsGreaterThanParent]}
|
||||||
defaultValue={defaults.newPerDay}
|
defaultValue={defaults.newPerDay}
|
||||||
|
@ -38,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<SpinBox
|
<SpinBox
|
||||||
label={tr.schedulingMaximumReviewsday()}
|
label={tr.schedulingMaximumReviewsday()}
|
||||||
tooltip={tr.deckConfigReviewLimitTooltip()}
|
tooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
||||||
min={0}
|
min={0}
|
||||||
warnings={[reviewsTooLow]}
|
warnings={[reviewsTooLow]}
|
||||||
defaultValue={defaults.reviewsPerDay}
|
defaultValue={defaults.reviewsPerDay}
|
||||||
|
|
68
ts/deckoptions/DisplayOrder.svelte
Normal file
68
ts/deckoptions/DisplayOrder.svelte
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as tr from "lib/i18n";
|
||||||
|
import EnumSelector from "./EnumSelector.svelte";
|
||||||
|
|
||||||
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
import { reviewMixChoices } from "./strings";
|
||||||
|
|
||||||
|
export let state: DeckOptionsState;
|
||||||
|
let config = state.currentConfig;
|
||||||
|
let defaults = state.defaults;
|
||||||
|
|
||||||
|
const newGatherPriorityChoices = [
|
||||||
|
tr.deckConfigNewGatherPriorityDeck(),
|
||||||
|
tr.deckConfigNewGatherPriorityPosition(),
|
||||||
|
];
|
||||||
|
const newSortOrderChoices = [
|
||||||
|
tr.deckConfigSortOrderCardTemplateThenPosition(),
|
||||||
|
tr.deckConfigSortOrderCardTemplateThenRandom(),
|
||||||
|
tr.deckConfigSortOrderPosition(),
|
||||||
|
tr.deckConfigSortOrderRandom(),
|
||||||
|
];
|
||||||
|
const reviewOrderChoices = [
|
||||||
|
tr.deckConfigSortOrderDueDateThenRandom(),
|
||||||
|
tr.deckConfigSortOrderAscendingIntervals(),
|
||||||
|
tr.deckConfigSortOrderDescendingIntervals(),
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>{tr.deckConfigOrderingTitle()}</h2>
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.deckConfigNewGatherPriority()}
|
||||||
|
tooltip={tr.deckConfigNewGatherPriorityTooltip()}
|
||||||
|
choices={newGatherPriorityChoices}
|
||||||
|
defaultValue={defaults.newCardGatherPriority}
|
||||||
|
bind:value={$config.newCardGatherPriority} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.deckConfigNewCardSortOrder()}
|
||||||
|
tooltip={tr.deckConfigNewCardSortOrderTooltip()}
|
||||||
|
choices={newSortOrderChoices}
|
||||||
|
defaultValue={defaults.newCardSortOrder}
|
||||||
|
bind:value={$config.newCardSortOrder} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.deckConfigNewReviewPriority()}
|
||||||
|
tooltip={tr.deckConfigNewReviewPriorityTooltip()}
|
||||||
|
choices={reviewMixChoices()}
|
||||||
|
defaultValue={defaults.newMix}
|
||||||
|
bind:value={$config.newMix} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.deckConfigInterdayStepPriority()}
|
||||||
|
tooltip={tr.deckConfigInterdayStepPriorityTooltip()}
|
||||||
|
choices={reviewMixChoices()}
|
||||||
|
defaultValue={defaults.interdayLearningMix}
|
||||||
|
bind:value={$config.interdayLearningMix} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.deckConfigReviewSortOrder()}
|
||||||
|
tooltip={tr.deckConfigReviewSortOrderTooltip()}
|
||||||
|
choices={reviewOrderChoices}
|
||||||
|
defaultValue={defaults.reviewOrder}
|
||||||
|
bind:value={$config.reviewOrder} />
|
|
@ -1,33 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import * as tr from "lib/i18n";
|
|
||||||
import StepsInput from "./StepsInput.svelte";
|
|
||||||
import EnumSelector from "./EnumSelector.svelte";
|
|
||||||
import type { DeckOptionsState } from "./lib";
|
|
||||||
import { reviewMixChoices } from "./strings";
|
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
|
||||||
let config = state.currentConfig;
|
|
||||||
let defaults = state.defaults;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h2>{tr.deckConfigLearningTitle()}</h2>
|
|
||||||
|
|
||||||
<StepsInput
|
|
||||||
label={tr.deckConfigLearningSteps()}
|
|
||||||
tooltip={tr.deckConfigLearningStepsTooltip()}
|
|
||||||
defaultValue={defaults.learnSteps}
|
|
||||||
value={$config.learnSteps}
|
|
||||||
on:changed={(evt) => ($config.learnSteps = evt.detail.value)} />
|
|
||||||
|
|
||||||
{#if state.v3Scheduler}
|
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigInterdayStepPriority()}
|
|
||||||
tooltip={tr.deckConfigInterdayStepPriorityTooltip()}
|
|
||||||
choices={reviewMixChoices()}
|
|
||||||
defaultValue={defaults.interdayLearningMix}
|
|
||||||
bind:value={$config.interdayLearningMix} />
|
|
||||||
{/if}
|
|
|
@ -5,10 +5,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "lib/i18n";
|
import * as tr from "lib/i18n";
|
||||||
import SpinBox from "./SpinBox.svelte";
|
import SpinBox from "./SpinBox.svelte";
|
||||||
import CheckBox from "./CheckBox.svelte";
|
import StepsInput from "./StepsInput.svelte";
|
||||||
import EnumSelector from "./EnumSelector.svelte";
|
import EnumSelector from "./EnumSelector.svelte";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import { reviewMixChoices } from "./strings";
|
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
let config = state.currentConfig;
|
let config = state.currentConfig;
|
||||||
|
@ -18,16 +17,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
tr.deckConfigNewInsertionOrderSequential(),
|
tr.deckConfigNewInsertionOrderSequential(),
|
||||||
tr.deckConfigNewInsertionOrderRandom(),
|
tr.deckConfigNewInsertionOrderRandom(),
|
||||||
];
|
];
|
||||||
const newGatherPriorityChoices = [
|
|
||||||
tr.deckConfigNewGatherPriorityDeck(),
|
|
||||||
tr.deckConfigNewGatherPriorityPosition(),
|
|
||||||
];
|
|
||||||
const newSortOrderChoices = [
|
|
||||||
tr.deckConfigSortOrderCardTemplateThenPosition(),
|
|
||||||
tr.deckConfigSortOrderCardTemplateThenRandom(),
|
|
||||||
tr.deckConfigSortOrderPosition(),
|
|
||||||
tr.deckConfigSortOrderRandom(),
|
|
||||||
];
|
|
||||||
|
|
||||||
let stepsExceedGraduatingInterval: string;
|
let stepsExceedGraduatingInterval: string;
|
||||||
$: {
|
$: {
|
||||||
|
@ -48,6 +37,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<h2>{tr.schedulingNewCards()}</h2>
|
<h2>{tr.schedulingNewCards()}</h2>
|
||||||
|
|
||||||
|
<StepsInput
|
||||||
|
label={tr.deckConfigLearningSteps()}
|
||||||
|
tooltip={tr.deckConfigLearningStepsTooltip()}
|
||||||
|
defaultValue={defaults.learnSteps}
|
||||||
|
value={$config.learnSteps}
|
||||||
|
on:changed={(evt) => ($config.learnSteps = evt.detail.value)} />
|
||||||
|
|
||||||
<SpinBox
|
<SpinBox
|
||||||
label={tr.schedulingGraduatingInterval()}
|
label={tr.schedulingGraduatingInterval()}
|
||||||
tooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
tooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
||||||
|
@ -62,38 +58,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
defaultValue={defaults.graduatingIntervalEasy}
|
defaultValue={defaults.graduatingIntervalEasy}
|
||||||
bind:value={$config.graduatingIntervalEasy} />
|
bind:value={$config.graduatingIntervalEasy} />
|
||||||
|
|
||||||
{#if state.v3Scheduler}
|
<EnumSelector
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigNewInsertionOrder()}
|
label={tr.deckConfigNewInsertionOrder()}
|
||||||
tooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
tooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
||||||
choices={newInsertOrderChoices}
|
choices={newInsertOrderChoices}
|
||||||
defaultValue={defaults.newCardInsertOrder}
|
defaultValue={defaults.newCardInsertOrder}
|
||||||
bind:value={$config.newCardInsertOrder} />
|
bind:value={$config.newCardInsertOrder} />
|
||||||
|
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigNewGatherPriority()}
|
|
||||||
tooltip={tr.deckConfigNewGatherPriorityTooltip()}
|
|
||||||
choices={newGatherPriorityChoices}
|
|
||||||
defaultValue={defaults.newCardGatherPriority}
|
|
||||||
bind:value={$config.newCardGatherPriority} />
|
|
||||||
|
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigSortOrder()}
|
|
||||||
tooltip={tr.deckConfigSortOrderTooltip()}
|
|
||||||
choices={newSortOrderChoices}
|
|
||||||
defaultValue={defaults.newCardSortOrder}
|
|
||||||
bind:value={$config.newCardSortOrder} />
|
|
||||||
|
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigReviewPriority()}
|
|
||||||
tooltip={tr.deckConfigReviewPriorityTooltip()}
|
|
||||||
choices={reviewMixChoices()}
|
|
||||||
defaultValue={defaults.newMix}
|
|
||||||
bind:value={$config.newMix} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
label={tr.deckConfigBuryNewSiblings()}
|
|
||||||
tooltip={tr.deckConfigBuryTooltip()}
|
|
||||||
defaultValue={defaults.buryNew}
|
|
||||||
bind:value={$config.buryNew} />
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import * as tr from "lib/i18n";
|
import * as tr from "lib/i18n";
|
||||||
import { revertIcon } from "./icons";
|
import { revertIcon } from "./icons";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { isEqual, cloneDeep } from "lodash-es";
|
import { isEqual as isEqualLodash, cloneDeep } from "lodash-es";
|
||||||
// import { onMount } from "svelte";
|
// import { onMount } from "svelte";
|
||||||
// import Tooltip from "bootstrap/js/dist/tooltip";
|
// import Tooltip from "bootstrap/js/dist/tooltip";
|
||||||
|
|
||||||
|
@ -26,6 +26,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function isEqual(a: unknown, b: unknown): boolean {
|
||||||
|
if (typeof a === "number" && typeof b === "number") {
|
||||||
|
// round to .01 precision before comparing,
|
||||||
|
// so the values coming out of the UI match
|
||||||
|
// the originals
|
||||||
|
return isEqualLodash(Math.round(a * 100) / 100, Math.round(b * 100) / 100);
|
||||||
|
} else {
|
||||||
|
return isEqualLodash(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let modified: boolean;
|
let modified: boolean;
|
||||||
$: modified = !isEqual(value, defaultValue);
|
$: modified = !isEqual(value, defaultValue);
|
||||||
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import * as tr from "lib/i18n";
|
|
||||||
import CheckBox from "./CheckBox.svelte";
|
|
||||||
import EnumSelector from "./EnumSelector.svelte";
|
|
||||||
import type { DeckOptionsState } from "./lib";
|
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
|
||||||
let config = state.currentConfig;
|
|
||||||
let defaults = state.defaults;
|
|
||||||
|
|
||||||
const reviewOrderChoices = [
|
|
||||||
tr.deckConfigSortOrderDueDateThenRandom(),
|
|
||||||
tr.deckConfigSortOrderAscendingIntervals(),
|
|
||||||
tr.deckConfigSortOrderDescendingIntervals(),
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2>{tr.schedulingReviews()}</h2>
|
|
||||||
|
|
||||||
{#if state.v3Scheduler}
|
|
||||||
<EnumSelector
|
|
||||||
label={tr.deckConfigSortOrder()}
|
|
||||||
tooltip={tr.deckConfigReviewSortOrderTooltip()}
|
|
||||||
choices={reviewOrderChoices}
|
|
||||||
defaultValue={defaults.reviewOrder}
|
|
||||||
bind:value={$config.reviewOrder} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
label={tr.deckConfigBuryReviewSiblings()}
|
|
||||||
tooltip={tr.deckConfigBuryTooltip()}
|
|
||||||
defaultValue={defaults.buryReviews}
|
|
||||||
bind:value={$config.buryReviews} />
|
|
||||||
</div>
|
|
|
@ -46,10 +46,10 @@ html {
|
||||||
// the default code color in tooltips is difficult to read; we'll probably
|
// the default code color in tooltips is difficult to read; we'll probably
|
||||||
// want to add more of our own styling in the future
|
// want to add more of our own styling in the future
|
||||||
code {
|
code {
|
||||||
color: var(--flag1-bg);
|
color: #ffaaaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
// override the default down arrow colour in <select> elements
|
// override the default down arrow colour in <select> elements
|
||||||
.night-mode select {
|
.night-mode select {
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e")
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,11 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut shortcut={'Control+Alt?+Shift+C'} let:createShortcut let:shortcutLabel>
|
||||||
shortcut="Control+Shift+KeyC"
|
|
||||||
optionalModifiers={['Alt']}
|
|
||||||
let:createShortcut
|
|
||||||
let:shortcutLabel>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
||||||
on:click={onCloze}
|
on:click={onCloze}
|
||||||
|
|
|
@ -36,7 +36,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup {api}>
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="F7" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'F7'} let:createShortcut let:shortcutLabel>
|
||||||
<IconButton
|
<IconButton
|
||||||
class="forecolor"
|
class="forecolor"
|
||||||
tooltip={appendInParentheses(tr.editingSetForegroundColor(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingSetForegroundColor(), shortcutLabel)}
|
||||||
|
@ -48,7 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="F8" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'F8'} let:createShortcut let:shortcutLabel>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
tooltip={appendInParentheses(tr.editingChangeColor(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingChangeColor(), shortcutLabel)}
|
||||||
on:change={setWithCurrentColor}
|
on:change={setWithCurrentColor}
|
||||||
|
|
|
@ -26,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup {api}>
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+KeyB" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+B'} let:createShortcut let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
key="bold"
|
key="bold"
|
||||||
update={() => document.queryCommandState('bold')}
|
update={() => document.queryCommandState('bold')}
|
||||||
|
@ -47,7 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+KeyI" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+I'} let:createShortcut let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
key="italic"
|
key="italic"
|
||||||
update={() => document.queryCommandState('italic')}
|
update={() => document.queryCommandState('italic')}
|
||||||
|
@ -68,7 +68,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+KeyU" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+U'} let:createShortcut let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
key="underline"
|
key="underline"
|
||||||
update={() => document.queryCommandState('underline')}
|
update={() => document.queryCommandState('underline')}
|
||||||
|
@ -89,10 +89,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut
|
<WithShortcut shortcut={'Control+='} let:createShortcut let:shortcutLabel>
|
||||||
shortcut="Control+Shift+Equal"
|
|
||||||
let:createShortcut
|
|
||||||
let:shortcutLabel>
|
|
||||||
<WithState
|
<WithState
|
||||||
key="superscript"
|
key="superscript"
|
||||||
update={() => document.queryCommandState('superscript')}
|
update={() => document.queryCommandState('superscript')}
|
||||||
|
@ -113,7 +110,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+Equal" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+Shift+='} let:createShortcut let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
key="subscript"
|
key="subscript"
|
||||||
update={() => document.queryCommandState('subscript')}
|
update={() => document.queryCommandState('subscript')}
|
||||||
|
@ -134,7 +131,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+KeyR" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+R'} let:createShortcut let:shortcutLabel>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={appendInParentheses(tr.editingRemoveFormatting(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingRemoveFormatting(), shortcutLabel)}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
|
@ -25,7 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="Control+KeyL" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+L'} let:createShortcut let:shortcutLabel>
|
||||||
<LabelButton
|
<LabelButton
|
||||||
disables={false}
|
disables={false}
|
||||||
tooltip={`${tr.editingCustomizeCardTemplates()} (${shortcutLabel})`}
|
tooltip={`${tr.editingCustomizeCardTemplates()} (${shortcutLabel})`}
|
||||||
|
|
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import LabelButton from "components/LabelButton.svelte";
|
import LabelButton from "components/LabelButton.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithShortcut shortcut="Control+Shift+KeyP" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'Control+Shift+P'} let:createShortcut let:shortcutLabel>
|
||||||
<LabelButton
|
<LabelButton
|
||||||
tooltip={tr.browsingPreviewSelectedCard({ val: shortcutLabel })}
|
tooltip={tr.browsingPreviewSelectedCard({ val: shortcutLabel })}
|
||||||
disables={false}
|
disables={false}
|
||||||
|
|
|
@ -36,7 +36,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup {api}>
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="F3" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'F3'} let:createShortcut let:shortcutLabel>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={appendInParentheses(tr.editingAttachPicturesaudiovideo(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingAttachPicturesaudiovideo(), shortcutLabel)}
|
||||||
on:click={onAttachment}
|
on:click={onAttachment}
|
||||||
|
@ -47,7 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut shortcut="F5" let:createShortcut let:shortcutLabel>
|
<WithShortcut shortcut={'F5'} let:createShortcut let:shortcutLabel>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)}
|
||||||
on:click={onRecord}
|
on:click={onRecord}
|
||||||
|
@ -69,7 +69,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<DropdownMenu id={menuId}>
|
<DropdownMenu id={menuId}>
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyM, KeyM"
|
shortcut={'Control+M, M'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -81,7 +81,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithShortcut>
|
</WithShortcut>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyM, KeyE"
|
shortcut={'Control+M, E'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -93,7 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithShortcut>
|
</WithShortcut>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyM, KeyC"
|
shortcut={'Control+M, C'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -105,7 +105,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithShortcut>
|
</WithShortcut>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyT, KeyT"
|
shortcut={'Control+T, T'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -117,7 +117,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithShortcut>
|
</WithShortcut>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyT, KeyE"
|
shortcut={'Control+T, E'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -129,7 +129,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithShortcut>
|
</WithShortcut>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut="Control+KeyT, KeyM"
|
shortcut={'Control+T, M'}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
@ -144,10 +144,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut
|
<WithShortcut shortcut={'Control+Shift+X'} let:createShortcut let:shortcutLabel>
|
||||||
shortcut="Control+Shift+KeyX"
|
|
||||||
let:createShortcut
|
|
||||||
let:shortcutLabel>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={appendInParentheses(tr.editingHtmlEditor(), shortcutLabel)}
|
tooltip={appendInParentheses(tr.editingHtmlEditor(), shortcutLabel)}
|
||||||
on:click={onHtmlEdit}
|
on:click={onHtmlEdit}
|
||||||
|
|
|
@ -66,8 +66,7 @@ function updateFocus(evt: FocusEvent) {
|
||||||
|
|
||||||
registerShortcut(
|
registerShortcut(
|
||||||
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
||||||
"Tab",
|
"Shift?+Tab"
|
||||||
["Shift"]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export function onKeyUp(evt: KeyboardEvent): void {
|
export function onKeyUp(evt: KeyboardEvent): void {
|
||||||
|
|
|
@ -4,8 +4,6 @@ import * as tr from "./i18n";
|
||||||
|
|
||||||
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
||||||
|
|
||||||
const modifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
|
||||||
|
|
||||||
function isApplePlatform(): boolean {
|
function isApplePlatform(): boolean {
|
||||||
return (
|
return (
|
||||||
window.navigator.platform.startsWith("Mac") ||
|
window.navigator.platform.startsWith("Mac") ||
|
||||||
|
@ -18,10 +16,6 @@ const platformModifiers = isApplePlatform()
|
||||||
? ["Meta", "Alt", "Shift", "Control"]
|
? ["Meta", "Alt", "Shift", "Control"]
|
||||||
: ["Control", "Alt", "Shift", "OS"];
|
: ["Control", "Alt", "Shift", "OS"];
|
||||||
|
|
||||||
function splitKeyCombinationString(keyCombinationString: string): string[][] {
|
|
||||||
return keyCombinationString.split(", ").map((segment) => segment.split("+"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function modifiersToPlatformString(modifiers: string[]): string {
|
function modifiersToPlatformString(modifiers: string[]): string {
|
||||||
const displayModifiers = isApplePlatform()
|
const displayModifiers = isApplePlatform()
|
||||||
? ["^", "⌥", "⇧", "⌘"]
|
? ["^", "⌥", "⇧", "⌘"]
|
||||||
|
@ -36,38 +30,50 @@ function modifiersToPlatformString(modifiers: string[]): string {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alphabeticPrefix = "Key";
|
const keyCodeLookup = {
|
||||||
const numericPrefix = "Digit";
|
Backspace: 8,
|
||||||
const keyToCharacterMap = {
|
Delete: 46,
|
||||||
Backslash: "\\",
|
Tab: 9,
|
||||||
Backquote: "`",
|
Enter: 13,
|
||||||
BracketLeft: "[",
|
F1: 112,
|
||||||
BrackerRight: "]",
|
F2: 113,
|
||||||
Quote: "'",
|
F3: 114,
|
||||||
Semicolon: ";",
|
F4: 115,
|
||||||
Minus: "-",
|
F5: 116,
|
||||||
Equal: "=",
|
F6: 117,
|
||||||
Comma: ",",
|
F7: 118,
|
||||||
Period: ".",
|
F8: 119,
|
||||||
Slash: "/",
|
F9: 120,
|
||||||
|
F10: 121,
|
||||||
|
F11: 122,
|
||||||
|
F12: 123,
|
||||||
|
"=": 187,
|
||||||
|
"-": 189,
|
||||||
|
"[": 219,
|
||||||
|
"]": 221,
|
||||||
|
"\\": 220,
|
||||||
|
";": 186,
|
||||||
|
"'": 222,
|
||||||
|
",": 188,
|
||||||
|
".": 190,
|
||||||
|
"/": 191,
|
||||||
|
"`": 192,
|
||||||
};
|
};
|
||||||
|
|
||||||
function keyToPlatformString(key: string): string {
|
function isRequiredModifier(modifier: string): boolean {
|
||||||
if (key.startsWith(alphabeticPrefix)) {
|
return !modifier.endsWith("?");
|
||||||
return key.slice(alphabeticPrefix.length);
|
|
||||||
} else if (key.startsWith(numericPrefix)) {
|
|
||||||
return key.slice(numericPrefix.length);
|
|
||||||
} else if (Object.prototype.hasOwnProperty.call(keyToCharacterMap, key)) {
|
|
||||||
return keyToCharacterMap[key];
|
|
||||||
} else {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPlatformString(modifiersAndKey: string[]): string {
|
function splitKeyCombinationString(keyCombinationString: string): string[][] {
|
||||||
return `${modifiersToPlatformString(
|
return keyCombinationString.split(", ").map((segment) => segment.split("+"));
|
||||||
modifiersAndKey.slice(0, -1)
|
}
|
||||||
)}${keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1])}`;
|
|
||||||
|
function toPlatformString(keyCombination: string[]): string {
|
||||||
|
return (
|
||||||
|
modifiersToPlatformString(
|
||||||
|
keyCombination.slice(0, -1).filter(isRequiredModifier)
|
||||||
|
) + keyCombination[keyCombination.length - 1]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlatformString(keyCombinationString: string): string {
|
export function getPlatformString(keyCombinationString: string): string {
|
||||||
|
@ -76,78 +82,107 @@ export function getPlatformString(keyCombinationString: string): string {
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkKey(event: KeyboardEvent, key: string): boolean {
|
function checkKey(event: KeyboardEvent, key: number): boolean {
|
||||||
return event.code === key;
|
return event.which === key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkModifiers(
|
const allModifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
||||||
event: KeyboardEvent,
|
|
||||||
optionalModifiers: Modifier[],
|
function partition<T>(predicate: (t: T) => boolean, items: T[]): [T[], T[]] {
|
||||||
activeModifiers: string[]
|
const trueItems: T[] = [];
|
||||||
): boolean {
|
const falseItems: T[] = [];
|
||||||
return modifiers.reduce(
|
|
||||||
(matches: boolean, modifier: string, currentIndex: number): boolean =>
|
items.forEach((t) => {
|
||||||
|
const target = predicate(t) ? trueItems : falseItems;
|
||||||
|
target.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [trueItems, falseItems];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTrailing(modifier: string): string {
|
||||||
|
return modifier.substring(0, modifier.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkModifiers(event: KeyboardEvent, modifiers: string[]): boolean {
|
||||||
|
const [requiredModifiers, otherModifiers] = partition(
|
||||||
|
isRequiredModifier,
|
||||||
|
modifiers
|
||||||
|
);
|
||||||
|
|
||||||
|
const optionalModifiers = otherModifiers.map(removeTrailing);
|
||||||
|
|
||||||
|
return allModifiers.reduce(
|
||||||
|
(matches: boolean, currentModifier: string, currentIndex: number): boolean =>
|
||||||
matches &&
|
matches &&
|
||||||
(optionalModifiers.includes(modifier as Modifier) ||
|
(optionalModifiers.includes(currentModifier as Modifier) ||
|
||||||
event.getModifierState(platformModifiers[currentIndex]) ===
|
event.getModifierState(platformModifiers[currentIndex]) ===
|
||||||
activeModifiers.includes(modifier)),
|
requiredModifiers.includes(currentModifier)),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function check(
|
const check = (keyCode: number, modifiers: string[]) => (
|
||||||
event: KeyboardEvent,
|
event: KeyboardEvent
|
||||||
optionalModifiers: Modifier[],
|
): boolean => {
|
||||||
modifiersAndKey: string[]
|
return checkKey(event, keyCode) && checkModifiers(event, modifiers);
|
||||||
): boolean {
|
};
|
||||||
return (
|
|
||||||
checkKey(event, modifiersAndKey[modifiersAndKey.length - 1]) &&
|
function keyToCode(key: string): number {
|
||||||
checkModifiers(event, optionalModifiers, modifiersAndKey.slice(0, -1))
|
return keyCodeLookup[key] || key.toUpperCase().charCodeAt(0);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortcutTimeoutMs = 400;
|
function keyCombinationToCheck(
|
||||||
|
keyCombination: string[]
|
||||||
|
): (event: KeyboardEvent) => boolean {
|
||||||
|
const keyCode = keyToCode(keyCombination[keyCombination.length - 1]);
|
||||||
|
const modifiers = keyCombination.slice(0, -1);
|
||||||
|
|
||||||
|
return check(keyCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GENERAL_KEY = 0;
|
||||||
|
const NUMPAD_KEY = 3;
|
||||||
|
|
||||||
function innerShortcut(
|
function innerShortcut(
|
||||||
lastEvent: KeyboardEvent,
|
lastEvent: KeyboardEvent,
|
||||||
callback: (event: KeyboardEvent) => void,
|
callback: (event: KeyboardEvent) => void,
|
||||||
optionalModifiers: Modifier[],
|
...checks: ((event: KeyboardEvent) => boolean)[]
|
||||||
...keyCombination: string[][]
|
|
||||||
): void {
|
): void {
|
||||||
let interval: number;
|
let interval: number;
|
||||||
|
|
||||||
if (keyCombination.length === 0) {
|
if (checks.length === 0) {
|
||||||
callback(lastEvent);
|
callback(lastEvent);
|
||||||
} else {
|
} else {
|
||||||
const [nextKey, ...restKeys] = keyCombination;
|
const [nextCheck, ...restChecks] = checks;
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
const handler = (event: KeyboardEvent): void => {
|
||||||
if (check(event, optionalModifiers, nextKey)) {
|
if (nextCheck(event)) {
|
||||||
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
innerShortcut(event, callback, ...restChecks);
|
||||||
clearTimeout(interval);
|
clearTimeout(interval);
|
||||||
|
} else if (
|
||||||
|
event.location === GENERAL_KEY ||
|
||||||
|
event.location === NUMPAD_KEY
|
||||||
|
) {
|
||||||
|
// Any non-modifier key will cancel the shortcut sequence
|
||||||
|
document.removeEventListener("keydown", handler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interval = setTimeout(
|
|
||||||
(): void => document.removeEventListener("keydown", handler),
|
|
||||||
shortcutTimeoutMs
|
|
||||||
);
|
|
||||||
|
|
||||||
document.addEventListener("keydown", handler, { once: true });
|
document.addEventListener("keydown", handler, { once: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerShortcut(
|
export function registerShortcut(
|
||||||
callback: (event: KeyboardEvent) => void,
|
callback: (event: KeyboardEvent) => void,
|
||||||
keyCombinationString: string,
|
keyCombinationString: string
|
||||||
optionalModifiers: Modifier[] = []
|
|
||||||
): () => void {
|
): () => void {
|
||||||
const keyCombination = splitKeyCombinationString(keyCombinationString);
|
const [check, ...restChecks] = splitKeyCombinationString(keyCombinationString).map(
|
||||||
const [firstKey, ...restKeys] = keyCombination;
|
keyCombinationToCheck
|
||||||
|
);
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
const handler = (event: KeyboardEvent): void => {
|
||||||
if (check(event, optionalModifiers, firstKey)) {
|
if (check(event)) {
|
||||||
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
innerShortcut(event, callback, ...restChecks);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue