diff --git a/defs.bzl b/defs.bzl
index ee63a1ed0..dcabd1a34 100644
--- a/defs.bzl
+++ b/defs.bzl
@@ -10,7 +10,7 @@ load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories")
load("//python/pyqt:defs.bzl", "install_pyqt")
load("@rules_python//python:pip.bzl", "pip_parse")
-anki_version = "2.1.50"
+anki_version = "2.1.51"
def setup_deps():
bazel_skylib_workspace()
diff --git a/ftl/.gitignore b/ftl/.gitignore
new file mode 100644
index 000000000..738260199
--- /dev/null
+++ b/ftl/.gitignore
@@ -0,0 +1,2 @@
+usage
+
diff --git a/ftl/core/custom-study.ftl b/ftl/core/custom-study.ftl
index 254338546..06b61ff30 100644
--- a/ftl/core/custom-study.ftl
+++ b/ftl/core/custom-study.ftl
@@ -29,5 +29,3 @@ custom-study-available-review-cards = Available review cards: { $count }
## DEPRECATED - you do not need to translate these.
-custom-study-new-cards-in-deck-over-today = New cards in deck over today limit: { $val }
-custom-study-reviews-due-in-deck-over-today = Reviews due in deck over today limit: { $val }
diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl
index d10ab8ae3..f42cf7900 100644
--- a/ftl/core/deck-config.ftl
+++ b/ftl/core/deck-config.ftl
@@ -122,28 +122,27 @@ deck-config-new-gather-priority-random-notes = Random notes
deck-config-new-gather-priority-random-cards = Random cards
deck-config-new-card-sort-order = New card sort order
deck-config-new-card-sort-order-tooltip-2 =
- `Card template`: Displays cards in card template order. If you have sibling burying
- disabled, this will ensure all front->back cards are seen before any back->front cards.
+ `Card type`: Displays cards in order of card type number. If you have sibling burying
+ disabled, this will ensure all front→back cards are seen before any back→front cards.
+ This is useful to have all cards of the same note shown in the same session, but not
+ too close to one another.
`Order gathered`: Shows cards exactly as they were gathered. If sibling burying is disabled,
this will typically result in all cards of a note being seen one after the other.
- `Card template, then random`: Like `Card template`, but shuffles the cards of each
- template. When combined with an ascending position gather order, this can be used to show
- the oldest cards in a random order for example.
+ `Card type, then random`: Like `Card type`, but shuffles the cards of each card
+ type number. If you use `Ascending position` to gather the oldest cards, you could use
+ this setting to see those cards in a random order, but still ensure cards of the same
+ note do not end up too close to one another.
- `Random note, then card template`: Picks notes at random, then shows all of their siblings
+ `Random note, then card type`: Picks notes at random, then shows all of their siblings
in order.
`Random`: Fully shuffles the gathered cards.
-deck-config-sort-order-card-template-then-lowest-position = Card template, then ascending position
-deck-config-sort-order-card-template-then-highest-position = Card template, then descending position
-deck-config-sort-order-card-template-then-random = Card template, then random
-deck-config-sort-order-random-note-then-template = Random note, then card template
-deck-config-sort-order-lowest-position = Ascending position
-deck-config-sort-order-highest-position = Descending position
+deck-config-sort-order-card-template-then-random = Card type, then random
+deck-config-sort-order-random-note-then-template = Random note, then card type
deck-config-sort-order-random = Random
-deck-config-sort-order-template-then-gather = Card template
+deck-config-sort-order-template-then-gather = Card type
deck-config-sort-order-gather = Order gathered
deck-config-new-review-priority = New/review order
deck-config-new-review-priority-tooltip = When to show new cards in relation to review cards.
@@ -171,6 +170,7 @@ deck-config-sort-order-ascending-intervals = Ascending intervals
deck-config-sort-order-descending-intervals = Descending intervals
deck-config-sort-order-ascending-ease = Ascending ease
deck-config-sort-order-descending-ease = Descending ease
+deck-config-sort-order-relative-overdueness = Relative overdueness
deck-config-display-order-will-use-current-deck =
Anki will use the display order from the deck you
select to study, and not any subdecks it may have.
@@ -273,15 +273,3 @@ deck-config-which-deck = Which deck would you like?
## NO NEED TO TRANSLATE. These strings have been replaced with new versions, and will be removed in the future.
-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-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 cards appear in strict position (due #) order, even if the parent limit is
- not high enough to see cards from all decks.
diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl
index f91dface4..df5d391db 100644
--- a/ftl/core/editing.ftl
+++ b/ftl/core/editing.ftl
@@ -60,5 +60,3 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no
## You don't need to translate these strings, as they will be replaced with different ones soon.
editing-html-editor = HTML Editor
-editing-set-text-color = Set text color
-editing-set-text-highlight-color = Set text highlight color
diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl
index 2d4523204..76e3132a0 100644
--- a/ftl/core/preferences.ftl
+++ b/ftl/core/preferences.ftl
@@ -1,19 +1,15 @@
preferences-automatically-sync-on-profile-openclose = Automatically sync on profile open/close
preferences-backups = Backups
-preferences-backups2 = backups
-preferences-backupsanki-will-create-a-backup-of =
Backups
Anki will create a backup of your collection each time it is closed.
preferences-basic = Basic
preferences-change-deck-depending-on-note-type = Change deck depending on note type
preferences-changes-will-take-effect-when-you = Changes will take effect when you restart Anki.
preferences-hours-past-midnight = hours past midnight
preferences-interface-language = Interface language:
preferences-interrupt-current-audio-when-answering = Interrupt current audio when answering
-preferences-keep = Keep
preferences-learn-ahead-limit = Learn ahead limit
preferences-mins = mins
preferences-network = Network
preferences-next-day-starts-at = Next day starts at
-preferences-night-mode = Night mode
preferences-note-media-is-not-backed-up = Note: Media is not backed up. Please create a periodic backup of your Anki folder to be safe.
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
diff --git a/ftl/qt/errors.ftl b/ftl/qt/errors.ftl
index 8990a65fc..5e88d4fea 100644
--- a/ftl/qt/errors.ftl
+++ b/ftl/qt/errors.ftl
@@ -43,4 +43,3 @@ errors-windows-tts-runtime-error = The TTS service failed. Please ensure Windows
## OBSOLETE; you do not need to translate this
--errors-addon-support-site = [add-on support site](https://help.ankiweb.net/discussions/add-ons/)
diff --git a/ftl/qt/qt-accel.ftl b/ftl/qt/qt-accel.ftl
index 27d421f99..e6fc8022f 100644
--- a/ftl/qt/qt-accel.ftl
+++ b/ftl/qt/qt-accel.ftl
@@ -1,5 +1,4 @@
qt-accel-about = &About
-qt-accel-browse-and-install = &Browse and Install...
qt-accel-cards = &Cards
qt-accel-check-database = &Check Database
qt-accel-check-media = Check &Media
@@ -21,7 +20,6 @@ qt-accel-invert-selection = &Invert Selection
qt-accel-next-card = &Next Card
qt-accel-note = N&ote
qt-accel-notes = &Notes
-qt-accel-open-addons-folder = &Open Add-ons Folder
qt-accel-preferences = &Preferences
qt-accel-previous-card = &Previous Card
qt-accel-select-all = Select &All
diff --git a/ftl/qt/qt-misc.ftl b/ftl/qt/qt-misc.ftl
index e91c9290e..17afa95d2 100644
--- a/ftl/qt/qt-misc.ftl
+++ b/ftl/qt/qt-misc.ftl
@@ -53,7 +53,6 @@ qt-misc-the-requested-change-will-require-a = The requested change will require
qt-misc-there-must-be-at-least-one = There must be at least one profile.
qt-misc-this-file-exists-are-you-sure = This file exists. Are you sure you want to overwrite it?
qt-misc-unable-to-access-anki-media-folder = Unable to access Anki media folder. The permissions on your system's temporary folder may be incorrect.
-qt-misc-unable-to-move-existing-file-to = Unable to move existing file to trash - please try restarting your computer.
qt-misc-unexpected-response-code = Unexpected response code: { $val }
qt-misc-would-you-like-to-download-it = Would you like to download it now?
qt-misc-your-collection-file-appears-to-be = Your collection file appears to be corrupt. This can happen when the file is copied or moved while Anki is open, or when the collection is stored on a network or cloud drive. If problems persist after restarting your computer, please open an automatic backup from the profile screen.
diff --git a/ftl/remove-unused.sh b/ftl/remove-unused.sh
index f4d932157..73ec9f926 100755
--- a/ftl/remove-unused.sh
+++ b/ftl/remove-unused.sh
@@ -1,5 +1,14 @@
#!/bin/bash
-
+#
+# To use, run:
+#
+# - ./update-ankimobile-usage.sh
+# - ./remove-unused.sh
+#
+# If you need to maintain compatibility with an older stable branch, you
+# can use ./update-desktop-usage.sh in the older release, then copy the
+# generated file into usage/ with a different name.
+#
# Caveats:
# - Messages are considered in use if they are referenced in other messages,
# even if those messages themselves are not in use and going to be deleted.
diff --git a/ftl/update-ankimobile-usage.sh b/ftl/update-ankimobile-usage.sh
index c14e56edc..e68620f04 100755
--- a/ftl/update-ankimobile-usage.sh
+++ b/ftl/update-ankimobile-usage.sh
@@ -7,7 +7,7 @@
set -e
scriptRoot=$(realpath $(dirname $0)/..)
-sourceRoot=$(realpath $scriptRoot/../mob/src)
+sourceRoot=$(realpath $scriptRoot/../../mobile/ankimobile/src)
bazel run //rslib/i18n_helpers:write_ftl_json $scriptRoot/ftl/usage/ankimobile.json \
$sourceRoot
diff --git a/ftl/usage/.gitignore b/ftl/usage/.gitignore
deleted file mode 100644
index f1be7f494..000000000
--- a/ftl/usage/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-desktop-head.json
diff --git a/ftl/usage/ankimobile.json b/ftl/usage/ankimobile.json
deleted file mode 100644
index fb4963acb..000000000
--- a/ftl/usage/ankimobile.json
+++ /dev/null
@@ -1,574 +0,0 @@
-[
- "about-anki-written-by",
- "about-license-text",
- "about-please-see",
- "about-some-thirdparty-libraries",
- "about-thanks-contributors",
- "about-thanks-for-support",
- "about-the-anki-homepage",
- "actions-actions",
- "actions-add",
- "actions-add-new",
- "actions-add-short",
- "actions-added-to-frequent-actions",
- "actions-all-actions",
- "actions-already-in-frequent-actions",
- "actions-an-error-occurred",
- "actions-answer-again",
- "actions-answer-again-short",
- "actions-answer-easy",
- "actions-answer-easy-short",
- "actions-answer-good",
- "actions-answer-good-short",
- "actions-answer-hard",
- "actions-answer-hard-short",
- "actions-are-you-sure",
- "actions-auto-advance",
- "actions-auto-advance-short",
- "actions-bottom-bar",
- "actions-bottom-bar-short",
- "actions-browse",
- "actions-browse-short",
- "actions-bury-card-short",
- "actions-bury-note-short",
- "actions-cancel",
- "actions-card-info",
- "actions-card-info-short",
- "actions-card-template",
- "actions-card-template-short",
- "actions-close",
- "actions-confirm-delete",
- "actions-current-audio-minus5s",
- "actions-current-audio-minus5s-short",
- "actions-current-audio-plus5s",
- "actions-current-audio-plus5s-short",
- "actions-custom-study",
- "actions-custom-study-cant-be-used-on",
- "actions-custom-study-short",
- "actions-deck-statistics",
- "actions-deck-statistics-short",
- "actions-decks",
- "actions-decks-short",
- "actions-delete",
- "actions-delete-note-short",
- "actions-discard-changes",
- "actions-done",
- "actions-downloading",
- "actions-drag-here-to-remove",
- "actions-draw",
- "actions-edit-short",
- "actions-empty-short",
- "actions-export",
- "actions-file-invalid-or-corrupt",
- "actions-filter",
- "actions-filter-short",
- "actions-filtercram",
- "actions-flag-blue",
- "actions-flag-green",
- "actions-flag-number",
- "actions-flag-orange",
- "actions-flag-pink",
- "actions-flag-purple",
- "actions-flag-red",
- "actions-flag-turquoise",
- "actions-frequent-actions",
- "actions-import",
- "actions-leave-without-saving",
- "actions-long-press-on-an-item-to",
- "actions-mark",
- "actions-mark-and-bury",
- "actions-mark-and-bury-short",
- "actions-mark-and-suspend",
- "actions-mark-and-suspend-short",
- "actions-mark-short",
- "actions-new-name",
- "actions-night-mode-short",
- "actions-no-current-card",
- "actions-no-load-restore-backup",
- "actions-not-valid-link",
- "actions-nothing-to-redo",
- "actions-nothing-to-undo",
- "actions-off",
- "actions-off-short",
- "actions-options",
- "actions-options-for",
- "actions-pause-audio-short",
- "actions-please-tap-the-in-the",
- "actions-preview",
- "actions-processing",
- "actions-rebuild",
- "actions-rebuild-short",
- "actions-record-voice",
- "actions-record-voice-menu",
- "actions-record-voice-short",
- "actions-redo-short",
- "actions-rename",
- "actions-rename-deck",
- "actions-replay-audio",
- "actions-replay-audio-short",
- "actions-replay-voice-short",
- "actions-reset-card",
- "actions-reset-card-short",
- "actions-revert",
- "actions-review-undone",
- "actions-save",
- "actions-scratchpad",
- "actions-scratchpad-short",
- "actions-scratchpad-size",
- "actions-scratchpad-size-short",
- "actions-search",
- "actions-select-deck",
- "actions-select-note-type",
- "actions-set-due-date",
- "actions-set-due-date-short",
- "actions-show-answer-short",
- "actions-show-answeranswer-good",
- "actions-show-answeranswer-good-short",
- "actions-study-options",
- "actions-study-options-short",
- "actions-suspend-card",
- "actions-suspend-card-short",
- "actions-suspend-note-short",
- "actions-tools",
- "actions-tools-overlay",
- "actions-tools-short",
- "actions-top-bar",
- "actions-top-bar-short",
- "actions-unbury-deck",
- "actions-unbury-deck-short",
- "actions-undo",
- "actions-undo-short",
- "adding-added",
- "adding-cloze-outside-cloze-field",
- "adding-cloze-outside-cloze-notetype",
- "adding-the-first-field-is-empty",
- "adding-you-have-a-cloze-deletion-note",
- "browsing-added-today",
- "browsing-again-today",
- "browsing-any-flag",
- "browsing-append",
- "browsing-append-negated",
- "browsing-card-updated",
- "browsing-cards-deleted",
- "browsing-cards-updated",
- "browsing-change-deck",
- "browsing-change-flag",
- "browsing-change-note-type",
- "browsing-change-notetype",
- "browsing-clear-flag",
- "browsing-clear-unused-tags",
- "browsing-column1",
- "browsing-column2",
- "browsing-confirm-reset",
- "browsing-current-deck",
- "browsing-dd-selected",
- "browsing-delete-notes",
- "browsing-due-dateorder",
- "browsing-due-reviews",
- "browsing-ease",
- "browsing-filtered",
- "browsing-find",
- "browsing-find-and-replace",
- "browsing-interval",
- "browsing-learning-cards",
- "browsing-no-cards-are-selected",
- "browsing-no-flag",
- "browsing-note-created",
- "browsing-note-updated",
- "browsing-note2",
- "browsing-notes-updated",
- "browsing-question",
- "browsing-questionandanswer",
- "browsing-removed-unused-tags-count",
- "browsing-repetitions",
- "browsing-replace-with",
- "browsing-reschedule",
- "browsing-reset-cards",
- "browsing-row-deleted",
- "browsing-searching",
- "browsing-second-column19",
- "browsing-second-column91",
- "browsing-select-all",
- "browsing-sort",
- "browsing-sort-field",
- "browsing-sort-order",
- "browsing-studied-today",
- "browsing-suspended",
- "browsing-tag",
- "browsing-toggle-suspend",
- "browsing-whole-collection",
- "card-stats-note-type",
- "card-templates-flip",
- "card-templates-night-mode",
- "card-templates-template-styling",
- "custom-study-any-tag",
- "custom-study-available",
- "custom-study-cant-extend-limits-no-extra",
- "custom-study-cram-seen-cards-with-certain-tags",
- "custom-study-custom-study-session",
- "custom-study-days-to-look-ahead",
- "custom-study-days-to-look-back",
- "custom-study-exclude-cards-with-tag",
- "custom-study-extra-new-cards",
- "custom-study-extra-review-cards",
- "custom-study-include-cards-with-tag",
- "custom-study-increase-todays-new-card-limit",
- "custom-study-increase-todays-review-card-limit",
- "custom-study-learn-new-cards-with-certain-tags",
- "custom-study-loading",
- "custom-study-max-cards-to-gather",
- "custom-study-no-tags",
- "custom-study-no-tags-available",
- "custom-study-ok",
- "custom-study-preview-all-cards-with-certain-tags",
- "custom-study-preview-new-cards",
- "custom-study-review-ahead",
- "custom-study-review-due-cards-with-certain-tags",
- "custom-study-review-forgotten-cards",
- "custom-study-search-matches",
- "custom-study-select",
- "custom-study-study-type",
- "custom-study-the-selected-options-did-not-match",
- "database-check-rebuilt",
- "database-check-title",
- "deck-config-title",
- "deck-config-used-by-decks",
- "deck-options-add-options-group",
- "deck-options-answer-time-cap",
- "deck-options-bury-related-new-cards",
- "deck-options-bury-related-reviews",
- "deck-options-defaults",
- "deck-options-delete-options-full-sync",
- "deck-options-display-in-order-added",
- "deck-options-display-in-random-order",
- "deck-options-full-sync-required",
- "deck-options-group-name",
- "deck-options-max-new-per-day",
- "deck-options-max-reviews-per-day",
- "deck-options-new-cards",
- "deck-options-options-group",
- "deck-options-replay-q-audio-in-answer",
- "deck-options-reset-all-settings-to-defaults",
- "deck-options-restore-defaults",
- "deck-options-steps",
- "decks-a-deck-must-be-provided",
- "decks-a-deck-named-already-exists",
- "decks-add-empty-deck",
- "decks-addexport",
- "decks-build",
- "decks-card-limit",
- "decks-custom-steps",
- "decks-deck",
- "decks-deck-label",
- "decks-deck-name",
- "decks-deck-options",
- "decks-download-link",
- "decks-enable-second-filter",
- "decks-export-collection",
- "decks-filter2",
- "decks-import-from-itunes",
- "decks-link-to-apkg-file-to-import",
- "decks-return-by-delete",
- "decks-shared-deck-list",
- "decks-study",
- "decks-sync",
- "decks-synchronize",
- "decks-the-provided-deck-does-not-exist",
- "decks-will-be-returned",
- "editing-add-media",
- "editing-bold",
- "editing-cant-edit-original-image-data",
- "editing-card-unsuspended",
- "editing-discard",
- "editing-discard-changes-question",
- "editing-fields",
- "editing-from-camera",
- "editing-from-file",
- "editing-from-photos",
- "editing-italic",
- "editing-keep-editing",
- "editing-next-cloze",
- "editing-next-field",
- "editing-note-type-prompt",
- "editing-note-unsuspended",
- "editing-same-cloze",
- "editing-tags",
- "editing-unable-to-obtain-image",
- "editing-unable-to-read-file",
- "editing-underline",
- "editing-unexpected-file-extension",
- "editing-unexpected-rich-text-format-please",
- "editing-unexpected-status-code",
- "editing-unsuspend-card",
- "editing-unsuspend-note",
- "empty-cards-delete-button",
- "empty-cards-delete-empty-cards",
- "empty-cards-delete-empty-notes",
- "empty-cards-deleted-count",
- "empty-cards-deleting",
- "empty-cards-not-found",
- "empty-cards-window-title",
- "errors100-tags-max",
- "exporting-collection-saved-to-itunes",
- "exporting-export-to-itunes",
- "exporting-export-to-share-sheet",
- "exporting-exporting",
- "exporting-include-media2",
- "exporting-media-files-exported-d",
- "findreplace-notes-updated",
- "importing-delete-imported-file",
- "importing-import-complete",
- "importing-importing",
- "importing-no-apkg-or-colpkg-files-were",
- "importing-overwrite-via-import",
- "importing-please-choose-a-file",
- "importing-processed-media-files-d",
- "importing-replace-collection",
- "media-check-check-media-action",
- "media-check-delete-unused",
- "media-check-delete-unused-complete",
- "media-check-empty-trash",
- "media-check-files-remaining",
- "media-check-restore-trash",
- "media-check-trash-emptied",
- "media-check-trash-restored",
- "media-check-window-title",
- "media-error-initializing-recorder",
- "media-error-playing-audio-full",
- "media-privacy-microphone",
- "media-recording",
- "notetypes-back-field",
- "notetypes-cloze-name",
- "notetypes-front-field",
- "preferences-about",
- "preferences-always-duck-and-ignore-mute",
- "preferences-answer-keeps-zoom",
- "preferences-answer-side",
- "preferences-audio-buttons",
- "preferences-auto-advance-answer-action",
- "preferences-auto-advance-answer-seconds",
- "preferences-auto-advance-auto-advance-to-start",
- "preferences-auto-advance-do-nothing",
- "preferences-auto-advance-question-seconds",
- "preferences-auto-advance-show-reminder",
- "preferences-auto-advance-wait-for-audio",
- "preferences-backup-available-backups",
- "preferences-backup-create-now",
- "preferences-backup-maximum-backups",
- "preferences-backup-minutes-between-backups",
- "preferences-backup-revert-to-backup",
- "preferences-backup-revert-to-backup-confirm",
- "preferences-backup-reverted-to-backup",
- "preferences-backups",
- "preferences-bottom-bar-size",
- "preferences-bottom-center",
- "preferences-bottom-left",
- "preferences-bottom-right",
- "preferences-button-d",
- "preferences-collection-day-starts",
- "preferences-collection-learn-ahead-minutes",
- "preferences-collection-mix",
- "preferences-collection-new-first",
- "preferences-collection-newreview-order",
- "preferences-collection-reviews-first",
- "preferences-double-tap-prevention",
- "preferences-drawing-screen-ignores-fingers",
- "preferences-editing",
- "preferences-editing-convert-smart-quotes",
- "preferences-editing-crop-camera-photos",
- "preferences-editing-max-image-size",
- "preferences-editing-resize-on-paste",
- "preferences-feedback-ticks",
- "preferences-fine",
- "preferences-force-sync-confirm",
- "preferences-full-sync",
- "preferences-gamepad-button-mapping",
- "preferences-gamepad-menu-button",
- "preferences-gamepads",
- "preferences-height",
- "preferences-huge",
- "preferences-ignore-fingers",
- "preferences-interrupt-current-audio",
- "preferences-large",
- "preferences-left",
- "preferences-left-shoulder",
- "preferences-left-thumbstick-button",
- "preferences-left-trigger",
- "preferences-logged-in-as",
- "preferences-long",
- "preferences-mid-center",
- "preferences-mid-left",
- "preferences-mid-right",
- "preferences-never-show-scratchpad",
- "preferences-never-show-scratchpad-enabled",
- "preferences-never-type-answer",
- "preferences-next-times",
- "preferences-normal",
- "preferences-notifications",
- "preferences-notifications-alert-time",
- "preferences-notifications-alert-when-due",
- "preferences-notifications-app-icon-shows-due-count",
- "preferences-notifications-settings-app-enable-notifications",
- "preferences-paste-clipboard-images-as-png",
- "preferences-pen-size",
- "preferences-preferences",
- "preferences-question-side",
- "preferences-remaining-count",
- "preferences-right",
- "preferences-right-shoulder",
- "preferences-right-thumbstick-button",
- "preferences-right-trigger",
- "preferences-scheduling",
- "preferences-scratchpad-below-buttons",
- "preferences-scratchpad-transparency",
- "preferences-scratchpad-transparency-full",
- "preferences-scratchpad-transparency-medium",
- "preferences-scratchpad-transparency-none",
- "preferences-scratchpad-transparency-slight",
- "preferences-shake-action",
- "preferences-short",
- "preferences-show-bottom-bar",
- "preferences-show-grid",
- "preferences-show-top-bar",
- "preferences-small",
- "preferences-swipe-down",
- "preferences-swipe-left",
- "preferences-swipe-right",
- "preferences-swipe-up",
- "preferences-swipes",
- "preferences-swipes-must-begin-from-the-far",
- "preferences-sync-sounds-images",
- "preferences-syncing",
- "preferences-tap-to-sync",
- "preferences-taps",
- "preferences-theme",
- "preferences-theme-bar-style",
- "preferences-theme-black",
- "preferences-theme-dark",
- "preferences-theme-dark-translucent",
- "preferences-theme-force-off",
- "preferences-theme-force-on",
- "preferences-theme-light-translucent",
- "preferences-theme-night-mode-desc",
- "preferences-theme-night-mode-same-as-system",
- "preferences-theme-slate",
- "preferences-thick",
- "preferences-tools-overlay-button",
- "preferences-tools-overlay-position",
- "preferences-top-center",
- "preferences-top-left",
- "preferences-top-right",
- "preferences-undo-clears-all",
- "preferences-when-answer-shown",
- "preferences-when-question-shown",
- "preferences-you-have-been-logged-out",
- "profiles-a-profile-with-that-name-already",
- "profiles-add-profile",
- "profiles-creating-backup",
- "profiles-finishing-backup",
- "profiles-please-provide-some-text-avoiding-symbols",
- "profiles-please-select-another-profile-first",
- "profiles-profile-name",
- "profiles-profiles",
- "profiles-rename-profile",
- "profiles-unable-to-open-safari-please",
- "profiles-user1",
- "profiles-welcome",
- "scheduling-automatically-play-audio",
- "scheduling-easy-bonus",
- "scheduling-easy-interval",
- "scheduling-end",
- "scheduling-forgot-cards",
- "scheduling-general",
- "scheduling-graduating-interval",
- "scheduling-interval-modifier",
- "scheduling-lapses",
- "scheduling-leech-action",
- "scheduling-leech-threshold",
- "scheduling-maximum-interval",
- "scheduling-minimum-interval",
- "scheduling-new-cards",
- "scheduling-new-interval",
- "scheduling-order",
- "scheduling-review",
- "scheduling-reviews",
- "scheduling-set-due-date-done",
- "scheduling-set-due-date-prompt",
- "scheduling-set-due-date-prompt-hint",
- "scheduling-starting-ease",
- "scheduling-steps-in-minutes",
- "scheduling-tag-only",
- "scheduling-update-button",
- "scheduling-update-done",
- "scheduling-update-later-button",
- "scheduling-update-more-info-button",
- "scheduling-update-soon",
- "statistics-answer-buttons-title",
- "statistics-reviews",
- "studying-again",
- "studying-answer-time-elapsed",
- "studying-auto-advance-starting",
- "studying-auto-advance-stopped",
- "studying-bury-card",
- "studying-bury-note",
- "studying-card-suspended",
- "studying-card-was-a-leech",
- "studying-cards-buried",
- "studying-delete-note",
- "studying-easy",
- "studying-edit",
- "studying-empty",
- "studying-finish",
- "studying-good",
- "studying-hard",
- "studying-have-ready-to-study",
- "studying-note-suspended",
- "studying-pause-audio",
- "studying-please-run-empty-cards-on-the",
- "studying-please-use-record-voice-first",
- "studying-replay-card",
- "studying-replay-voice",
- "studying-show-answer",
- "studying-suspend-note",
- "studying-type-in-the-answer",
- "sync-abort-button",
- "sync-cancel-button",
- "sync-checking",
- "sync-confirm-empty-download",
- "sync-conflict-explanation",
- "sync-connecting",
- "sync-download-from-ankiweb",
- "sync-downloading-from-ankiweb",
- "sync-email-address",
- "sync-log-out-button",
- "sync-login",
- "sync-media-aborted",
- "sync-media-complete",
- "sync-media-disabled",
- "sync-media-failed",
- "sync-media-is-syncing",
- "sync-media-log-button",
- "sync-media-log-title",
- "sync-media-missing-file",
- "sync-media-show-progress",
- "sync-media-starting",
- "sync-must-wait-for-end",
- "sync-password",
- "sync-please-enter-your-ankiweb-details",
- "sync-upload-to-ankiweb",
- "sync-uploading-to-ankiweb",
- "sync-wrong-pass",
- "undo-action-redone",
- "undo-action-undone",
- "undo-redo",
- "undo-redo-action",
- "undo-undo",
- "undo-undo-action",
- "urlscheme-a-note-type-must-be-provided",
- "urlscheme-a-note-with-the-same-first",
- "urlscheme-a-profile-must-be-provided",
- "urlscheme-added-note",
- "urlscheme-invalid-profile-name",
- "urlscheme-the-provided-note-type-does-not",
- "urlscheme-unable-to-open-profile"
-]
\ No newline at end of file
diff --git a/proto/anki/deckconfig.proto b/proto/anki/deckconfig.proto
index 755896f0e..86307d100 100644
--- a/proto/anki/deckconfig.proto
+++ b/proto/anki/deckconfig.proto
@@ -68,6 +68,7 @@ message DeckConfig {
REVIEW_CARD_ORDER_INTERVALS_DESCENDING = 4;
REVIEW_CARD_ORDER_EASE_ASCENDING = 5;
REVIEW_CARD_ORDER_EASE_DESCENDING = 6;
+ REVIEW_CARD_ORDER_RELATIVE_OVERDUENESS = 7;
}
enum ReviewMix {
REVIEW_MIX_MIX_WITH_REVIEWS = 0;
diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py
index ad66b4f7e..607792514 100644
--- a/pylib/anki/cards.py
+++ b/pylib/anki/cards.py
@@ -190,10 +190,13 @@ class Card(DeprecatedNamesMixin):
"autoplay"
]
- def time_taken(self) -> int:
- "Time taken to answer card, in integer MS."
+ def time_taken(self, capped: bool = True) -> int:
+ """Time taken since card timer started, in integer MS.
+ If `capped` is true, returned time is limited to deck preset setting."""
total = int((time.time() - self.timer_started) * 1000)
- return min(total, self.time_limit())
+ if capped:
+ total = min(total, self.time_limit())
+ return total
def description(self) -> str:
dict_copy = dict(self.__dict__)
diff --git a/pylib/anki/scheduler/v2.py b/pylib/anki/scheduler/v2.py
index 58f0a05cb..3167ae3f7 100644
--- a/pylib/anki/scheduler/v2.py
+++ b/pylib/anki/scheduler/v2.py
@@ -493,7 +493,7 @@ limit ?"""
card.did,
new_delta=new_delta,
review_delta=review_delta,
- milliseconds_delta=+card.time_taken(),
+ milliseconds_delta=card.time_taken(),
)
# once a card has been answered once, the original due date
diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py
index 10b588562..c23d26922 100644
--- a/pylib/anki/scheduler/v3.py
+++ b/pylib/anki/scheduler/v3.py
@@ -81,7 +81,7 @@ class Scheduler(SchedulerBaseWithLegacy):
new_state=new_state,
rating=rating,
answered_at_millis=int_time(1000),
- milliseconds_taken=card.time_taken(),
+ milliseconds_taken=card.time_taken(capped=False),
)
def answer_card(self, input: CardAnswer) -> OpChanges:
diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py
index e2b1926d0..bb070ea67 100644
--- a/qt/aqt/__init__.py
+++ b/qt/aqt/__init__.py
@@ -34,7 +34,7 @@ import locale
import os
import tempfile
import traceback
-from typing import Any, Callable, Optional, cast
+from typing import TYPE_CHECKING, Any, Callable, Optional, cast
import anki.lang
from anki._backend import RustBackend
@@ -46,6 +46,9 @@ from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import TR, tr
+if TYPE_CHECKING:
+ import aqt.profiles
+
# compat aliases
anki.version = _version # type: ignore
anki.Collection = Collection # type: ignore
@@ -56,8 +59,10 @@ try:
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
sys.stderr.reconfigure(encoding="utf-8") # type: ignore
except AttributeError:
- # on Windows without console, NullWriter doesn't support this
- pass
+ if is_win:
+ # On Windows without console; add a mock writer. The stderr
+ # writer will be overwritten when ErrorHandler is initialized.
+ sys.stderr = sys.stdout = open(os.devnull, "w", encoding="utf8")
appVersion = _version
appWebsite = "https://apps.ankiweb.net/"
@@ -361,9 +366,6 @@ def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
def setupGL(pm: aqt.profiles.ProfileManager) -> None:
- if is_mac:
- return
-
driver = pm.video_driver()
# work around pyqt loading wrong GL library
@@ -397,7 +399,12 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
context += f"{ctx.function}"
if context:
context = f"'{context}'"
- if "Failed to create OpenGL context" in msg:
+ if (
+ "Failed to create OpenGL context" in msg
+ # Based on the message Qt6 shows to the user; have not tested whether
+ # we can actually capture this or not.
+ or "Failed to initialize graphics backend" in msg
+ ):
QMessageBox.critical(
None,
tr.qt_misc_error(),
@@ -413,19 +420,23 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
qInstallMessageHandler(msgHandler)
- # ignore set graphics driver on Qt6 for now
- if qtmajor > 5:
- return
-
if driver == VideoDriver.OpenGL:
+ # Leaving QT_OPENGL unset appears to sometimes produce different results
+ # to explicitly setting it to 'auto'; the former seems to be more compatible.
pass
else:
if is_win:
+ # on Windows, this appears to be sufficient on Qt5/Qt6.
+ # On Qt6, ANGLE is excluded by the enum.
os.environ["QT_OPENGL"] = driver.value
elif is_mac:
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL)
elif is_lin:
+ # Qt5 only
os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1"
+ # Required on Qt6
+ if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ:
+ os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
@@ -488,7 +499,8 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp
and not os.getenv("ANKI_WAYLAND")
):
# users need to opt in to wayland support, given the issues it has
- print("Wayland support is disabled by default due to bugs.")
+ print("Wayland support is disabled by default due to bugs:")
+ print("https://github.com/ankitects/anki/issues/1767")
print("You can force it on with an env var: ANKI_WAYLAND=1")
os.environ["QT_QPA_PLATFORM"] = "xcb"
diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py
index b05778111..b5b0f1b19 100644
--- a/qt/aqt/addons.py
+++ b/qt/aqt/addons.py
@@ -360,6 +360,10 @@ class AddonManager:
return all_conflicts
def _disableConflicting(self, module: str, conflicts: list[str] = None) -> set[str]:
+ if not self.isEnabled(module):
+ # disabled add-ons should not trigger conflict handling
+ return set()
+
conflicts = conflicts or self.addonConflicts(module)
installed = self.allAddons()
@@ -388,7 +392,10 @@ class AddonManager:
return manifest
def install(
- self, file: IO | str, manifest: dict[str, Any] = None
+ self,
+ file: IO | str,
+ manifest: dict[str, Any] | None = None,
+ force_enable: bool = False,
) -> InstallOk | InstallError:
"""Install add-on from path or file-like object. Metadata is read
from the manifest file, with keys overriden by supplying a 'manifest'
@@ -418,6 +425,10 @@ class AddonManager:
k: v for k, v in manifest.items() if k in schema and schema[k]["meta"]
}
meta.update(manifest_meta)
+
+ if force_enable:
+ meta["disabled"] = False
+
self.writeAddonMeta(package, meta)
meta2 = self.addon_meta(package)
@@ -466,7 +477,10 @@ class AddonManager:
######################################################################
def processPackages(
- self, paths: list[str], parent: QWidget = None
+ self,
+ paths: list[str],
+ parent: QWidget | None = None,
+ force_enable: bool = False,
) -> tuple[list[str], list[str]]:
log = []
@@ -476,7 +490,7 @@ class AddonManager:
try:
for path in paths:
base = os.path.basename(path)
- result = self.install(path)
+ result = self.install(path, force_enable=force_enable)
if isinstance(result, InstallError):
errs.extend(
@@ -905,7 +919,9 @@ class AddonsDialog(QDialog):
def onGetAddons(self) -> None:
obj = GetAddons(self)
if obj.ids:
- download_addons(self, self.mgr, obj.ids, self.after_downloading)
+ download_addons(
+ self, self.mgr, obj.ids, self.after_downloading, force_enable=True
+ )
def after_downloading(self, log: list[DownloadLogEntry]) -> None:
self.redrawAddons()
@@ -924,7 +940,7 @@ class AddonsDialog(QDialog):
if not paths:
return False
- installAddonPackages(self.mgr, paths, parent=self)
+ installAddonPackages(self.mgr, paths, parent=self, force_enable=True)
self.redrawAddons()
return None
@@ -1078,7 +1094,7 @@ def download_encountered_problem(log: list[DownloadLogEntry]) -> bool:
def download_and_install_addon(
- mgr: AddonManager, client: HttpClient, id: int
+ mgr: AddonManager, client: HttpClient, id: int, force_enable: bool = False
) -> DownloadLogEntry:
"Download and install a single add-on."
result = download_addon(client, id)
@@ -1099,7 +1115,9 @@ def download_and_install_addon(
branch_index=result.branch_index,
)
- result2 = mgr.install(io.BytesIO(result.data), manifest=manifest)
+ result2 = mgr.install(
+ io.BytesIO(result.data), manifest=manifest, force_enable=force_enable
+ )
return (id, result2)
@@ -1119,7 +1137,10 @@ class DownloaderInstaller(QObject):
self.client.progress_hook = bg_thread_progress
def download(
- self, ids: list[int], on_done: Callable[[list[DownloadLogEntry]], None]
+ self,
+ ids: list[int],
+ on_done: Callable[[list[DownloadLogEntry]], None],
+ force_enable: bool = False,
) -> None:
self.ids = ids
self.log: list[DownloadLogEntry] = []
@@ -1132,7 +1153,9 @@ class DownloaderInstaller(QObject):
parent = self.parent()
assert isinstance(parent, QWidget)
self.mgr.mw.progress.start(immediate=True, parent=parent)
- self.mgr.mw.taskman.run_in_background(self._download_all, self._download_done)
+ self.mgr.mw.taskman.run_in_background(
+ lambda: self._download_all(force_enable), self._download_done
+ )
def _progress_callback(self, up: int, down: int) -> None:
self.dl_bytes += down
@@ -1144,9 +1167,13 @@ class DownloaderInstaller(QObject):
)
)
- def _download_all(self) -> None:
+ def _download_all(self, force_enable: bool = False) -> None:
for id in self.ids:
- self.log.append(download_and_install_addon(self.mgr, self.client, id))
+ self.log.append(
+ download_and_install_addon(
+ self.mgr, self.client, id, force_enable=force_enable
+ )
+ )
def _download_done(self, future: Future) -> None:
self.mgr.mw.progress.finish()
@@ -1176,11 +1203,12 @@ def download_addons(
ids: list[int],
on_done: Callable[[list[DownloadLogEntry]], None],
client: HttpClient | None = None,
+ force_enable: bool = False,
) -> None:
if client is None:
client = HttpClient()
downloader = DownloaderInstaller(parent, mgr, client)
- downloader.download(ids, on_done=on_done)
+ downloader.download(ids, on_done=on_done, force_enable=force_enable)
# Update checking
@@ -1619,6 +1647,7 @@ def installAddonPackages(
warn: bool = False,
strictly_modal: bool = False,
advise_restart: bool = False,
+ force_enable: bool = False,
) -> bool:
if warn:
@@ -1639,7 +1668,9 @@ def installAddonPackages(
):
return False
- log, errs = addonsManager.processPackages(paths, parent=parent)
+ log, errs = addonsManager.processPackages(
+ paths, parent=parent, force_enable=force_enable
+ )
if log:
log_html = "
".join(log)
diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py
index 4a314feac..392e533d9 100644
--- a/qt/aqt/browser/browser.py
+++ b/qt/aqt/browser/browser.py
@@ -137,8 +137,8 @@ class Browser(QMainWindow):
self.on_undo_state_change(mw.undo_actions_info())
# legacy alias
self.model = MockModel(self)
- self.setupSearch(card, search)
gui_hooks.browser_will_show(self)
+ self.setupSearch(card, search)
self.show()
def on_operation_did_execute(
diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index ad6094009..d1a1ae233 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -159,6 +159,7 @@ class Editor:
context=self,
default_css=False,
)
+ self.web._fix_editor_background_color_and_show()
lefttopbtns: list[str] = []
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
diff --git a/qt/aqt/forms/clayout_top.ui b/qt/aqt/forms/clayout_top.ui
index 32a47a14f..60bab6226 100644
--- a/qt/aqt/forms/clayout_top.ui
+++ b/qt/aqt/forms/clayout_top.ui
@@ -49,6 +49,9 @@
0
+
+ 50
+
30
diff --git a/qt/aqt/main.py b/qt/aqt/main.py
index cfc7f2279..e9fbaa238 100644
--- a/qt/aqt/main.py
+++ b/qt/aqt/main.py
@@ -1207,6 +1207,7 @@ title="{}" {}>{}""".format(
advise_restart=not startup,
strictly_modal=startup,
parent=None if startup else self,
+ force_enable=True,
)
# Cramming
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index 8f09c3253..1d8ce51f5 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -38,28 +38,32 @@ class VideoDriver(Enum):
@staticmethod
def default_for_platform() -> VideoDriver:
- if is_mac:
+ if is_mac or qtmajor > 5:
return VideoDriver.OpenGL
else:
return VideoDriver.Software
def constrained_to_platform(self) -> VideoDriver:
- if self == VideoDriver.ANGLE and not is_win:
+ if self == VideoDriver.ANGLE and not VideoDriver.supports_angle():
return VideoDriver.Software
return self
def next(self) -> VideoDriver:
if self == VideoDriver.Software:
return VideoDriver.OpenGL
- elif self == VideoDriver.OpenGL and is_win:
+ elif self == VideoDriver.OpenGL and VideoDriver.supports_angle():
return VideoDriver.ANGLE
else:
return VideoDriver.Software
+ @staticmethod
+ def supports_angle() -> bool:
+ return is_win and qtmajor < 6
+
@staticmethod
def all_for_platform() -> list[VideoDriver]:
all = [VideoDriver.OpenGL]
- if is_win:
+ if VideoDriver.supports_angle():
all.append(VideoDriver.ANGLE)
all.append(VideoDriver.Software)
return all
@@ -475,7 +479,11 @@ create table if not exists profiles
######################################################################
def _gldriver_path(self) -> str:
- return os.path.join(self.base, "gldriver")
+ if qtmajor < 6:
+ fname = "gldriver"
+ else:
+ fname = "gldriver6"
+ return os.path.join(self.base, fname)
def video_driver(self) -> VideoDriver:
path = self._gldriver_path()
diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py
index afa1c3617..7892853a4 100644
--- a/qt/aqt/webview.py
+++ b/qt/aqt/webview.py
@@ -421,18 +421,22 @@ button:focus {{ outline: 5px auto {color_hl}; }}"""
elif is_mac:
family = "Helvetica"
font = f'font-size:15px;font-family:"{family}";'
- button_style = """
-button { -webkit-appearance: none; background: #fff; border: 1px solid #ccc;
+ color = ""
+ if not theme_manager.night_mode:
+ color = "background: #fff; border: 1px solid #ccc;"
+ button_style = (
+ """
+button { -webkit-appearance: none; %s
border-radius:5px; font-family: Helvetica }"""
+ % color
+ )
else:
family = self.font().family()
color_hl_txt = palette.color(QPalette.ColorRole.HighlightedText).name()
- color_btn = palette.color(QPalette.ColorRole.Button).name()
font = f'font-size:14px;font-family:"{family}", sans-serif;'
button_style = """
/* Buttons */
button{{
- background-color: {color_btn};
font-family:"{family}", sans-serif; }}
button:focus{{ border-color: {color_hl} }}
button:active, button:active:hover {{ background-color: {color_hl}; color: {color_hl_txt};}}
@@ -443,7 +447,6 @@ div[contenteditable="true"]:focus {{
border-color: {color_hl};
}}""".format(
family=family,
- color_btn=color_btn,
color_hl=color_hl,
color_hl_txt=color_hl_txt,
)
@@ -719,3 +722,30 @@ html {{ {font} }}
}})();
"""
)
+
+ def _fix_editor_background_color_and_show(self) -> None:
+ # The editor does not use our standard CSS, which takes care of matching the background
+ # colour of the webview to the window we're showing it in. This causes a difference in
+ # shades on Windows/Linux in day mode, that we need to work around. This is a temporary
+ # fix before the 2.1.50 release; with more time there may be a better way to do this.
+
+ if theme_manager.night_mode:
+ # The styling changes are not required for night mode, and hiding+showing the
+ # webview causes a flash of black.
+ return
+
+ self.hide()
+
+ window_bg_day = self.get_window_bg_color(False).name()
+ css = f":root {{ --window-bg: {window_bg_day} }}"
+ self.evalWithCallback(
+ f"""
+(function(){{
+ const style = document.createElement('style');
+ style.innerHTML = `{css}`;
+ document.head.appendChild(style);
+}})();
+""",
+ # avoids FOUC
+ lambda _: self.show(),
+ )
diff --git a/qt/bundle/win/anki-console.bat b/qt/bundle/win/anki-console.bat
index 7b488cdd9..76be8172b 100644
--- a/qt/bundle/win/anki-console.bat
+++ b/qt/bundle/win/anki-console.bat
@@ -1,5 +1,5 @@
@echo off
-anki
+anki %*
pause
diff --git a/repos.bzl b/repos.bzl
index e0877a60f..603e4ca64 100644
--- a/repos.bzl
+++ b/repos.bzl
@@ -115,12 +115,12 @@ def register_repos():
################
core_i18n_repo = "anki-core-i18n"
- core_i18n_commit = "7d1954863a721e09d974c609b55fa78f0e178b0f"
- core_i18n_zip_csum = "14cce5d0ecd2c00ce839d735ab7fe979439080ad9c510cc6fc2e63c97a9c745c"
+ core_i18n_commit = "d6f01a26657ae78371813c8f54e415dc62032a90"
+ core_i18n_zip_csum = "cd74a8f40a6080a144b6f80ad88bd37309ea4f17d4dd3995d154482eafc86f98"
qtftl_i18n_repo = "anki-desktop-ftl"
- qtftl_i18n_commit = "31baaae83fad4be3d8977d6053ef3032bdb90481"
- qtftl_i18n_zip_csum = "96e42e0278affb2586e677b52b466e6ca8bb4f3fd080a561683c48b483202fa2"
+ qtftl_i18n_commit = "76751e5dcb8bce78cd54b052f1b444531934ca45"
+ qtftl_i18n_zip_csum = "ad37c33d3d03a05b1094d6692300401a304da9ba48fb2dcf7ad01242ecc97997"
i18n_build_content = """
filegroup(
diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs
index 91024fff2..e3ee09096 100644
--- a/rslib/src/backend/scheduler/mod.rs
+++ b/rslib/src/backend/scheduler/mod.rs
@@ -182,7 +182,7 @@ impl SchedulerService for Backend {
}
fn answer_card(&self, input: pb::CardAnswer) -> Result {
- self.with_col(|col| col.answer_card(&input.into()))
+ self.with_col(|col| col.answer_card(&mut input.into()))
.map(Into::into)
}
diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs
index e9026757b..66b0fe406 100644
--- a/rslib/src/backend/search/mod.rs
+++ b/rslib/src/backend/search/mod.rs
@@ -13,7 +13,7 @@ use crate::{
backend_proto::sort_order::Value as SortOrderProto,
browser_table::Column,
prelude::*,
- search::{replace_search_node, Node, SortMode},
+ search::{replace_search_node, JoinSearches, Node, SortMode},
};
impl SearchService for Backend {
@@ -45,12 +45,11 @@ impl SearchService for Backend {
fn join_search_nodes(&self, input: pb::JoinSearchNodesRequest) -> Result {
let existing_node: Node = input.existing_node.unwrap_or_default().try_into()?;
let additional_node: Node = input.additional_node.unwrap_or_default().try_into()?;
- let search = SearchBuilder::from_root(existing_node);
Ok(
match pb::search_node::group::Joiner::from_i32(input.joiner).unwrap_or_default() {
- pb::search_node::group::Joiner::And => search.and(additional_node),
- pb::search_node::group::Joiner::Or => search.or(additional_node),
+ pb::search_node::group::Joiner::And => existing_node.and_flat(additional_node),
+ pb::search_node::group::Joiner::Or => existing_node.or_flat(additional_node),
}
.write()
.into(),
diff --git a/rslib/src/import_export/package/colpkg/import.rs b/rslib/src/import_export/package/colpkg/import.rs
index 2dbb49d95..e90a8b427 100644
--- a/rslib/src/import_export/package/colpkg/import.rs
+++ b/rslib/src/import_export/package/colpkg/import.rs
@@ -55,6 +55,7 @@ fn check_collection_and_mod_schema(col_path: &Path) -> Result<()> {
.ok()
.and_then(|mut col| {
col.set_schema_modified().ok()?;
+ col.set_modified().ok()?;
col.storage
.db
.pragma_query_value(None, "integrity_check", |row| row.get::<_, String>(0))
diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs
index 4da6c95e0..84ebb3e24 100644
--- a/rslib/src/notetype/mod.rs
+++ b/rslib/src/notetype/mod.rs
@@ -44,7 +44,7 @@ use crate::{
define_newtype,
error::{CardTypeError, CardTypeErrorDetails},
prelude::*,
- search::{Node, SearchNode},
+ search::{JoinSearches, Node, SearchNode},
storage::comma_separated_ids,
template::{FieldRequirements, ParsedTemplate},
text::ensure_string_in_nfc,
@@ -222,7 +222,7 @@ impl Collection {
.ok_or(AnkiError::NotFound)?;
if self
- .search_notes_unordered(SearchBuilder::from(note1.notetype_id).and(nids_node))?
+ .search_notes_unordered(note1.notetype_id.and(nids_node))?
.len()
!= note_ids.len()
{
diff --git a/rslib/src/notetype/notetypechange.rs b/rslib/src/notetype/notetypechange.rs
index 35c796b99..af13bf787 100644
--- a/rslib/src/notetype/notetypechange.rs
+++ b/rslib/src/notetype/notetypechange.rs
@@ -8,7 +8,7 @@ use std::collections::{HashMap, HashSet};
use super::{CardGenContext, Notetype, NotetypeKind};
use crate::{
prelude::*,
- search::{Node, SearchNode, SortMode, TemplateKind},
+ search::{JoinSearches, Node, SearchNode, SortMode, TemplateKind},
storage::comma_separated_ids,
};
@@ -294,10 +294,7 @@ impl Collection {
if !map.removed.is_empty() {
let ords =
SearchBuilder::any(map.removed.iter().map(|o| TemplateKind::Ordinal(*o as u16)));
- self.search_cards_into_table(
- SearchBuilder::from(nids).and(ords.group()),
- SortMode::NoOrder,
- )?;
+ self.search_cards_into_table(nids.and(ords), SortMode::NoOrder)?;
for card in self.storage.all_searched_cards()? {
self.remove_card_and_add_grave_undoable(card, usn)?;
}
@@ -319,10 +316,7 @@ impl Collection {
.keys()
.map(|o| TemplateKind::Ordinal(*o as u16)),
);
- self.search_cards_into_table(
- SearchBuilder::from(nids).and(ords.group()),
- SortMode::NoOrder,
- )?;
+ self.search_cards_into_table(nids.and(ords), SortMode::NoOrder)?;
for mut card in self.storage.all_searched_cards()? {
let original = card.clone();
card.template_idx =
diff --git a/rslib/src/notetype/schemachange.rs b/rslib/src/notetype/schemachange.rs
index cb68f69f2..7ca00faa4 100644
--- a/rslib/src/notetype/schemachange.rs
+++ b/rslib/src/notetype/schemachange.rs
@@ -8,7 +8,7 @@ use std::collections::HashMap;
use super::{CardGenContext, Notetype};
use crate::{
prelude::*,
- search::{SortMode, TemplateKind},
+ search::{JoinSearches, SortMode, TemplateKind},
};
/// True if any ordinals added, removed or reordered.
@@ -69,6 +69,7 @@ impl Collection {
if !ords_changed(&ords, previous_field_count) {
if nt.config.sort_field_idx != previous_sort_idx {
// only need to update sort field
+ self.set_schema_modified()?;
let nids = self.search_notes_unordered(nt.id)?;
for nid in nids {
let mut note = self.storage.get_note(nid)?.unwrap();
@@ -144,10 +145,7 @@ impl Collection {
// remove any cards where the template was deleted
if !changes.removed.is_empty() {
let ords = SearchBuilder::any(changes.removed.into_iter().map(TemplateKind::Ordinal));
- self.search_cards_into_table(
- SearchBuilder::from(nt.id).and(ords.group()),
- SortMode::NoOrder,
- )?;
+ self.search_cards_into_table(nt.id.and(ords), SortMode::NoOrder)?;
for card in self.storage.all_searched_cards()? {
self.remove_card_and_add_grave_undoable(card, usn)?;
}
@@ -157,10 +155,7 @@ impl Collection {
// update ordinals for cards with a repositioned template
if !changes.moved.is_empty() {
let ords = SearchBuilder::any(changes.moved.keys().cloned().map(TemplateKind::Ordinal));
- self.search_cards_into_table(
- SearchBuilder::from(nt.id).and(ords.group()),
- SortMode::NoOrder,
- )?;
+ self.search_cards_into_table(nt.id.and(ords), SortMode::NoOrder)?;
for mut card in self.storage.all_searched_cards()? {
let original = card.clone();
card.template_idx = *changes.moved.get(&card.template_idx).unwrap();
diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs
index 2eeb66252..6bc229c36 100644
--- a/rslib/src/scheduler/answering/mod.rs
+++ b/rslib/src/scheduler/answering/mod.rs
@@ -43,6 +43,12 @@ pub struct CardAnswer {
pub milliseconds_taken: u32,
}
+impl CardAnswer {
+ fn cap_answer_secs(&mut self, max_secs: u32) {
+ self.milliseconds_taken = self.milliseconds_taken.min(max_secs * 1000);
+ }
+}
+
/// Holds the information required to determine a given card's
/// current state, and to apply a state change to it.
struct CardStateUpdater {
@@ -238,11 +244,12 @@ impl Collection {
}
/// Answer card, writing its new state to the database.
- pub fn answer_card(&mut self, answer: &CardAnswer) -> Result> {
+ /// Provided [CardAnswer] has its answer time capped to deck preset.
+ pub fn answer_card(&mut self, answer: &mut CardAnswer) -> Result> {
self.transact(Op::AnswerCard, |col| col.answer_card_inner(answer))
}
- fn answer_card_inner(&mut self, answer: &CardAnswer) -> Result<()> {
+ fn answer_card_inner(&mut self, answer: &mut CardAnswer) -> Result<()> {
let card = self
.storage
.get_card(answer.card_id)?
@@ -251,6 +258,7 @@ impl Collection {
let usn = self.usn()?;
let mut updater = self.card_state_updater(card)?;
+ answer.cap_answer_secs(updater.config.inner.cap_answer_time_to_secs);
let current_state = updater.current_card_state();
if current_state != answer.current_state {
return Err(AnkiError::invalid_input(format!(
@@ -404,7 +412,7 @@ pub mod test_helpers {
{
let queued = self.get_next_card()?.unwrap();
let new_state = get_state(&queued.next_states);
- self.answer_card(&CardAnswer {
+ self.answer_card(&mut CardAnswer {
card_id: queued.card.id,
current_state: queued.next_states.current,
new_state,
diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs
index eff09c5ea..1ef3b6cf5 100644
--- a/rslib/src/scheduler/answering/preview.rs
+++ b/rslib/src/scheduler/answering/preview.rs
@@ -85,7 +85,7 @@ mod test {
));
// use Again on the preview
- col.answer_card(&CardAnswer {
+ col.answer_card(&mut CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.again,
@@ -99,7 +99,7 @@ mod test {
// hard
let next = col.get_next_card_states(c.id)?;
- col.answer_card(&CardAnswer {
+ col.answer_card(&mut CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.hard,
@@ -112,7 +112,7 @@ mod test {
// good
let next = col.get_next_card_states(c.id)?;
- col.answer_card(&CardAnswer {
+ col.answer_card(&mut CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.good,
@@ -125,7 +125,7 @@ mod test {
// and then it should return to its old state once easy selected
let next = col.get_next_card_states(c.id)?;
- col.answer_card(&CardAnswer {
+ col.answer_card(&mut CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.easy,
diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs
index 81114b90d..c3607d9c2 100644
--- a/rslib/src/scheduler/bury_and_suspend.rs
+++ b/rslib/src/scheduler/bury_and_suspend.rs
@@ -10,7 +10,7 @@ use crate::{
card::CardQueue,
config::SchedulerVersion,
prelude::*,
- search::{SearchNode, SortMode, StateKind},
+ search::{JoinSearches, SearchNode, SortMode, StateKind},
};
impl Card {
@@ -79,7 +79,7 @@ impl Collection {
};
self.transact(Op::UnburyUnsuspend, |col| {
col.search_cards_into_table(
- SearchBuilder::from(SearchNode::DeckIdWithChildren(deck_id)).and(state),
+ SearchNode::DeckIdWithChildren(deck_id).and(state),
SortMode::NoOrder,
)?;
col.unsuspend_or_unbury_searched_cards()
diff --git a/rslib/src/scheduler/filtered/custom_study.rs b/rslib/src/scheduler/filtered/custom_study.rs
index 1ab390bdc..e3dcf042b 100644
--- a/rslib/src/scheduler/filtered/custom_study.rs
+++ b/rslib/src/scheduler/filtered/custom_study.rs
@@ -13,7 +13,7 @@ use crate::{
decks::{FilteredDeck, FilteredSearchOrder, FilteredSearchTerm},
error::{CustomStudyError, FilteredDeckError},
prelude::*,
- search::{Negated, PropertyKind, RatingKind, SearchNode, StateKind},
+ search::{JoinSearches, Negated, PropertyKind, RatingKind, SearchNode, StateKind},
};
impl Collection {
@@ -184,29 +184,29 @@ fn custom_study_config(
}
fn forgot_config(deck_name: String, days: u32) -> FilteredDeck {
- let search = SearchBuilder::from(SearchNode::Rated {
+ let search = SearchNode::Rated {
days,
ease: RatingKind::AnswerButton(1),
- })
+ }
.and(SearchNode::from_deck_name(&deck_name))
.write();
custom_study_config(false, search, FilteredSearchOrder::Random, None)
}
fn ahead_config(deck_name: String, days: u32) -> FilteredDeck {
- let search = SearchBuilder::from(SearchNode::Property {
+ let search = SearchNode::Property {
operator: "<=".to_string(),
kind: PropertyKind::Due(days as i32),
- })
+ }
.and(SearchNode::from_deck_name(&deck_name))
.write();
custom_study_config(true, search, FilteredSearchOrder::Due, None)
}
fn preview_config(deck_name: String, days: u32) -> FilteredDeck {
- let search = SearchBuilder::from(StateKind::New)
- .and(SearchNode::AddedInDays(days))
- .and(SearchNode::from_deck_name(&deck_name))
+ let search = StateKind::New
+ .and_flat(SearchNode::AddedInDays(days))
+ .and_flat(SearchNode::from_deck_name(&deck_name))
.write();
custom_study_config(
false,
@@ -237,8 +237,8 @@ fn cram_config(deck_name: String, cram: &Cram) -> Result {
};
let search = nodes
- .and(tags_to_nodes(&cram.tags_to_include, &cram.tags_to_exclude))
.and(SearchNode::from_deck_name(&deck_name))
+ .and_flat(tags_to_nodes(&cram.tags_to_include, &cram.tags_to_exclude))
.write();
Ok(custom_study_config(
@@ -261,7 +261,7 @@ fn tags_to_nodes(tags_to_include: &[String], tags_to_exclude: &[String]) -> Sear
.map(|tag| SearchNode::from_tag_name(tag).negated()),
);
- include_nodes.group().and(exclude_nodes)
+ include_nodes.and(exclude_nodes)
}
#[cfg(test)]
@@ -357,4 +357,23 @@ mod test {
Ok(())
}
+
+ #[test]
+ fn sql_grouping() -> Result<()> {
+ let mut deck = preview_config("d".into(), 1);
+ assert_eq!(&deck.search_terms[0].search, "is:new added:1 deck:d");
+
+ let cram = Cram {
+ tags_to_include: vec!["1".into(), "2".into()],
+ tags_to_exclude: vec!["3".into(), "4".into()],
+ ..Default::default()
+ };
+ deck = cram_config("d".into(), &cram)?;
+ assert_eq!(
+ &deck.search_terms[0].search,
+ "is:due deck:d (tag:1 OR tag:2) (-tag:3 -tag:4)"
+ );
+
+ Ok(())
+ }
}
diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs
index 05a0d6999..421e6fb5b 100644
--- a/rslib/src/scheduler/new.rs
+++ b/rslib/src/scheduler/new.rs
@@ -14,7 +14,7 @@ use crate::{
config::{BoolKey, SchedulerVersion},
deckconfig::NewCardInsertOrder,
prelude::*,
- search::{SearchNode, SortMode, StateKind},
+ search::{JoinSearches, SearchNode, SortMode, StateKind},
};
impl Card {
@@ -267,7 +267,7 @@ impl Collection {
usn: Usn,
) -> Result {
let cids = self.search_cards(
- SearchBuilder::from(SearchNode::DeckIdWithoutChildren(deck)).and(StateKind::New),
+ SearchNode::DeckIdWithoutChildren(deck).and(StateKind::New),
SortMode::NoOrder,
)?;
self.sort_cards_inner(&cids, 1, 1, order.into(), false, usn)
diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs
index a60c95d57..8da5b11a9 100644
--- a/rslib/src/scheduler/queue/builder/mod.rs
+++ b/rslib/src/scheduler/queue/builder/mod.rs
@@ -265,6 +265,7 @@ mod test {
use super::*;
use crate::{
backend_proto::deck_config::config::{NewCardGatherPriority, NewCardSortOrder},
+ card::{CardQueue, CardType},
collection::open_test_collection,
};
@@ -296,10 +297,29 @@ mod test {
})
.collect()
}
+
+ fn set_deck_review_order(&mut self, deck: &mut Deck, order: ReviewCardOrder) {
+ let mut conf = DeckConfig::default();
+ conf.inner.review_order = order as i32;
+ self.add_or_update_deck_config(&mut conf).unwrap();
+ deck.normal_mut().unwrap().config_id = conf.id.0;
+ self.add_or_update_deck(deck).unwrap();
+ }
+
+ fn queue_as_due_and_ivl(&mut self, deck_id: DeckId) -> Vec<(i32, u32)> {
+ self.build_queues(deck_id)
+ .unwrap()
+ .iter()
+ .map(|entry| {
+ let card = self.storage.get_card(entry.card_id()).unwrap().unwrap();
+ (card.due, card.interval)
+ })
+ .collect()
+ }
}
#[test]
- fn queue_building() -> Result<()> {
+ fn new_queue_building() -> Result<()> {
let mut col = open_test_collection();
col.set_config_bool(BoolKey::Sched2021, true, false)?;
@@ -366,4 +386,52 @@ mod test {
Ok(())
}
+
+ #[test]
+ fn review_queue_building() -> Result<()> {
+ let mut col = open_test_collection();
+ col.set_config_bool(BoolKey::Sched2021, true, false)?;
+
+ let mut deck = col.get_or_create_normal_deck("Default").unwrap();
+ let nt = col.get_notetype_by_name("Basic")?.unwrap();
+ let mut cards = vec![];
+
+ // relative overdueness
+ let expected_queue = vec![
+ (-150, 1),
+ (-100, 1),
+ (-50, 1),
+ (-150, 5),
+ (-100, 5),
+ (-50, 5),
+ (-150, 20),
+ (-150, 20),
+ (-100, 20),
+ (-50, 20),
+ (-150, 100),
+ (-100, 100),
+ (-50, 100),
+ (0, 1),
+ (0, 5),
+ (0, 20),
+ (0, 100),
+ ];
+ for t in expected_queue.iter() {
+ let mut note = nt.new_note();
+ note.set_field(0, "foo")?;
+ note.id.0 = 0;
+ col.add_note(&mut note, deck.id)?;
+ let mut card = col.storage.get_card_by_ordinal(note.id, 0)?.unwrap();
+ card.interval = t.1;
+ card.due = t.0;
+ card.ctype = CardType::Review;
+ card.queue = CardQueue::Review;
+ cards.push(card);
+ }
+ col.update_cards_maybe_undoable(cards, false)?;
+ col.set_deck_review_order(&mut deck, ReviewCardOrder::RelativeOverdueness);
+ assert_eq!(col.queue_as_due_and_ivl(deck.id), expected_queue);
+
+ Ok(())
+ }
}
diff --git a/rslib/src/search/builder.rs b/rslib/src/search/builder.rs
index a234b27a5..85ee56dd7 100644
--- a/rslib/src/search/builder.rs
+++ b/rslib/src/search/builder.rs
@@ -12,6 +12,19 @@ pub trait Negated {
fn negated(self) -> Node;
}
+pub trait JoinSearches {
+ /// Concatenates two sets of [Node]s, inserting [Node::And], and grouping, if appropriate.
+ fn and(self, other: impl Into) -> SearchBuilder;
+ /// Concatenates two sets of [Node]s, inserting [Node::Or], and grouping, if appropriate.
+ fn or(self, other: impl Into) -> SearchBuilder;
+ /// Concatenates two sets of [Node]s, inserting [Node::And] if appropriate,
+ /// but without grouping either set.
+ fn and_flat(self, other: impl Into) -> SearchBuilder;
+ /// Concatenates two sets of [Node]s, inserting [Node::Or] if appropriate,
+ /// but without grouping either set.
+ fn or_flat(self, other: impl Into) -> SearchBuilder;
+}
+
impl> Negated for T {
fn negated(self) -> Node {
let node: Node = self.into();
@@ -23,6 +36,24 @@ impl> Negated for T {
}
}
+impl> JoinSearches for T {
+ fn and(self, other: impl Into) -> SearchBuilder {
+ self.into().join_other(other.into(), Node::And, true)
+ }
+
+ fn or(self, other: impl Into) -> SearchBuilder {
+ self.into().join_other(other.into(), Node::Or, true)
+ }
+
+ fn and_flat(self, other: impl Into) -> SearchBuilder {
+ self.into().join_other(other.into(), Node::And, false)
+ }
+
+ fn or_flat(self, other: impl Into) -> SearchBuilder {
+ self.into().join_other(other.into(), Node::Or, false)
+ }
+}
+
/// Helper to programmatically build searches.
#[derive(Debug, PartialEq, Clone)]
pub struct SearchBuilder(Vec);
@@ -59,19 +90,11 @@ impl SearchBuilder {
self.0.len()
}
- /// Concatenates the two sets of [Node]s, inserting [Node::And] if appropriate.
- /// No implicit grouping is done.
- pub fn and(self, other: impl Into) -> Self {
- self.join_other(other.into(), Node::And)
- }
-
- /// Concatenates the two sets of [Node]s, inserting [Node::Or] if appropriate.
- /// No implicit grouping is done.
- pub fn or(self, other: impl Into) -> Self {
- self.join_other(other.into(), Node::Or)
- }
-
- fn join_other(mut self, mut other: Self, joiner: Node) -> Self {
+ fn join_other(mut self, mut other: Self, joiner: Node, group: bool) -> Self {
+ if group {
+ self = self.group();
+ other = other.group();
+ }
if !(self.is_empty() || other.is_empty()) {
self.0.push(joiner);
}
@@ -193,4 +216,38 @@ mod test {
Node::Not(Box::new(Node::Search(SearchNode::State(StateKind::Due))))
)
}
+
+ #[test]
+ fn joining() {
+ assert_eq!(
+ StateKind::Due
+ .or(StateKind::New)
+ .and(SearchBuilder::any((1..4).map(SearchNode::Flag)))
+ .write(),
+ "(is:due OR is:new) (flag:1 OR flag:2 OR flag:3)"
+ );
+ assert_eq!(
+ StateKind::Due
+ .or(StateKind::New)
+ .and_flat(SearchBuilder::any((1..4).map(SearchNode::Flag)))
+ .write(),
+ "is:due OR is:new flag:1 OR flag:2 OR flag:3"
+ );
+ assert_eq!(
+ StateKind::Due
+ .or(StateKind::New)
+ .or(StateKind::Learning)
+ .or(StateKind::Review)
+ .write(),
+ "((is:due OR is:new) OR is:learn) OR is:review"
+ );
+ assert_eq!(
+ StateKind::Due
+ .or_flat(StateKind::New)
+ .or_flat(StateKind::Learning)
+ .or_flat(StateKind::Review)
+ .write(),
+ "is:due OR is:new OR is:learn OR is:review"
+ );
+ }
}
diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs
index c2c5f06c0..721c57b80 100644
--- a/rslib/src/search/mod.rs
+++ b/rslib/src/search/mod.rs
@@ -8,7 +8,7 @@ pub(crate) mod writer;
use std::borrow::Cow;
-pub use builder::{Negated, SearchBuilder};
+pub use builder::{JoinSearches, Negated, SearchBuilder};
pub use parser::{
parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind,
};
diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs
index c84584922..e28ceb0bd 100644
--- a/rslib/src/search/sqlwriter.rs
+++ b/rslib/src/search/sqlwriter.rs
@@ -113,6 +113,9 @@ impl SqlWriter<'_> {
}
}
+ // NOTE: when adding any new nodes in the future, make sure that they are either a single
+ // search term, or they wrap multiple terms in parentheses, as can be seen in the sql() unit
+ // test at the bottom of the file.
fn write_search_node_to_sql(&mut self, node: &SearchNode) -> Result<()> {
use normalize_to_nfc as norm;
match node {
diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs
index 906894007..6e9abe8de 100644
--- a/rslib/src/storage/card/mod.rs
+++ b/rslib/src/storage/card/mod.rs
@@ -4,7 +4,7 @@
pub(crate) mod data;
pub(crate) mod filtered;
-use std::{collections::HashSet, convert::TryFrom, result};
+use std::{collections::HashSet, convert::TryFrom, fmt, result};
use rusqlite::{
named_params, params,
@@ -244,7 +244,7 @@ impl super::SqliteStorage {
where
F: FnMut(DueCard) -> bool,
{
- let order_clause = review_order_sql(order);
+ let order_clause = review_order_sql(order, day_cutoff);
let mut stmt = self.db.prepare_cached(&format!(
"{} order by {}",
include_str!("due_cards.sql"),
@@ -668,11 +668,13 @@ enum ReviewOrderSubclause {
IntervalsDescending,
EaseAscending,
EaseDescending,
+ RelativeOverdueness { today: u32 },
}
-impl ReviewOrderSubclause {
- fn to_str(self) -> &'static str {
- match self {
+impl fmt::Display for ReviewOrderSubclause {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let temp_string;
+ let clause = match self {
ReviewOrderSubclause::Day => "due",
ReviewOrderSubclause::Deck => "(select rowid from active_decks ad where ad.id = did)",
ReviewOrderSubclause::Random => "fnvhash(id, mod)",
@@ -680,11 +682,16 @@ impl ReviewOrderSubclause {
ReviewOrderSubclause::IntervalsDescending => "ivl desc",
ReviewOrderSubclause::EaseAscending => "factor asc",
ReviewOrderSubclause::EaseDescending => "factor desc",
- }
+ ReviewOrderSubclause::RelativeOverdueness { today } => {
+ temp_string = format!("ivl / cast({today}-due+0.001 as real)", today = today);
+ &temp_string
+ }
+ };
+ write!(f, "{}", clause)
}
}
-fn review_order_sql(order: ReviewCardOrder) -> String {
+fn review_order_sql(order: ReviewCardOrder, today: u32) -> String {
let mut subclauses = match order {
ReviewCardOrder::Day => vec![ReviewOrderSubclause::Day],
ReviewCardOrder::DayThenDeck => vec![ReviewOrderSubclause::Day, ReviewOrderSubclause::Deck],
@@ -693,12 +700,15 @@ fn review_order_sql(order: ReviewCardOrder) -> String {
ReviewCardOrder::IntervalsDescending => vec![ReviewOrderSubclause::IntervalsDescending],
ReviewCardOrder::EaseAscending => vec![ReviewOrderSubclause::EaseAscending],
ReviewCardOrder::EaseDescending => vec![ReviewOrderSubclause::EaseDescending],
+ ReviewCardOrder::RelativeOverdueness => {
+ vec![ReviewOrderSubclause::RelativeOverdueness { today }]
+ }
};
subclauses.push(ReviewOrderSubclause::Random);
let v: Vec<_> = subclauses
- .into_iter()
- .map(ReviewOrderSubclause::to_str)
+ .iter()
+ .map(ReviewOrderSubclause::to_string)
.collect();
v.join(", ")
}
diff --git a/ts/deck-options/DisplayOrder.svelte b/ts/deck-options/DisplayOrder.svelte
index 33d77f5ff..3039df645 100644
--- a/ts/deck-options/DisplayOrder.svelte
+++ b/ts/deck-options/DisplayOrder.svelte
@@ -42,6 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
tr.deckConfigSortOrderDescendingIntervals(),
tr.deckConfigSortOrderAscendingEase(),
tr.deckConfigSortOrderDescendingEase(),
+ tr.deckConfigSortOrderRelativeOverdueness(),
];
const GatherOrder = DeckConfig.DeckConfig.Config.NewCardGatherPriority;
diff --git a/ts/editable/Mathjax.svelte b/ts/editable/Mathjax.svelte
index 80102898c..cc56a1b2c 100644
--- a/ts/editable/Mathjax.svelte
+++ b/ts/editable/Mathjax.svelte
@@ -41,11 +41,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let image: HTMLImageElement;
- export function moveCaretAfter(): void {
+ export function moveCaretAfter(position?: [number, number]): void {
// This should trigger a focusing of the Mathjax Handle
image.dispatchEvent(
new CustomEvent("movecaretafter", {
- detail: image,
+ detail: { image, position },
bubbles: true,
composed: true,
}),
diff --git a/ts/editable/mathjax-element.ts b/ts/editable/mathjax-element.ts
index 1cfbcfdba..f2b24e335 100644
--- a/ts/editable/mathjax-element.ts
+++ b/ts/editable/mathjax-element.ts
@@ -121,7 +121,15 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
});
if (this.hasAttribute("focusonmount")) {
- this.component.moveCaretAfter();
+ let position: [number, number] | undefined = undefined;
+
+ if (this.getAttribute("focusonmount")!.length > 0) {
+ position = this.getAttribute("focusonmount")!
+ .split(",")
+ .map(Number) as [number, number];
+ }
+
+ this.component.moveCaretAfter(position);
}
this.setAttribute("contentEditable", "false");
diff --git a/ts/editor/CodeMirror.svelte b/ts/editor/CodeMirror.svelte
index 204d22161..f9eaeeb41 100644
--- a/ts/editor/CodeMirror.svelte
+++ b/ts/editor/CodeMirror.svelte
@@ -30,6 +30,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let configuration: CodeMirrorLib.EditorConfiguration;
export let code: Writable;
+ export let hidden = false;
const defaultConfiguration = {
rtlMoveVisually: true,
@@ -50,9 +51,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const direction = getContext>(directionKey);
- $: setOption("direction", $direction);
- $: setOption("theme", $pageTheme.isDark ? darkTheme : lightTheme);
-
let apiPartial: Partial;
export { apiPartial as api };
@@ -77,8 +75,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
tabindex="-1"
hidden
use:openCodeMirror={{
- configuration: { ...configuration, ...defaultConfiguration },
+ configuration: {
+ ...configuration,
+ ...defaultConfiguration,
+ direction: $direction,
+ theme: $pageTheme.isDark ? darkTheme : lightTheme,
+ },
resolve,
+ hidden,
}}
/>
diff --git a/ts/editor/code-mirror.ts b/ts/editor/code-mirror.ts
index 91f137ba7..a11becc83 100644
--- a/ts/editor/code-mirror.ts
+++ b/ts/editor/code-mirror.ts
@@ -52,34 +52,51 @@ export const gutterOptions: CodeMirror.EditorConfiguration = {
foldGutter: true,
};
-export function focusAndCaretAfter(editor: CodeMirror.Editor): void {
+export function focusAndSetCaret(
+ editor: CodeMirror.Editor,
+ position: CodeMirror.Position = { line: editor.lineCount(), ch: 0 },
+): void {
editor.focus();
- editor.setCursor(editor.lineCount(), 0);
+ editor.setCursor(position);
}
interface OpenCodeMirrorOptions {
configuration: CodeMirror.EditorConfiguration;
resolve(editor: CodeMirror.EditorFromTextArea): void;
+ hidden: boolean;
}
export function openCodeMirror(
textarea: HTMLTextAreaElement,
- { configuration, resolve }: Partial,
+ options: Partial,
): { update: (options: Partial) => void; destroy: () => void } {
- const editor = CodeMirror.fromTextArea(textarea, configuration);
- resolve?.(editor);
+ let editor: CodeMirror.EditorFromTextArea | null = null;
- return {
- update({ configuration }: Partial): void {
+ function update({
+ configuration,
+ resolve,
+ hidden,
+ }: Partial): void {
+ if (editor) {
for (const key in configuration) {
editor.setOption(
key as keyof CodeMirror.EditorConfiguration,
configuration[key],
);
}
- },
+ } else if (!hidden) {
+ editor = CodeMirror.fromTextArea(textarea, configuration);
+ resolve?.(editor);
+ }
+ }
+
+ update(options);
+
+ return {
+ update,
destroy(): void {
- editor.toTextArea();
+ editor?.toTextArea();
+ editor = null;
},
};
}
diff --git a/ts/editor/editor-toolbar/HighlightColorButton.svelte b/ts/editor/editor-toolbar/HighlightColorButton.svelte
index 661e96ac2..f1313d54e 100644
--- a/ts/editor/editor-toolbar/HighlightColorButton.svelte
+++ b/ts/editor/editor-toolbar/HighlightColorButton.svelte
@@ -130,7 +130,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{
color = setColor(event);
- bridgeCommand(`lastTextColor:${color}`);
+ bridgeCommand(`lastHighlightColor:${color}`);
}}
on:change={() => setTextColor()}
/>
diff --git a/ts/editor/editor-toolbar/LatexButton.svelte b/ts/editor/editor-toolbar/LatexButton.svelte
index a4c831388..ffd535dc1 100644
--- a/ts/editor/editor-toolbar/LatexButton.svelte
+++ b/ts/editor/editor-toolbar/LatexButton.svelte
@@ -35,7 +35,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
function onMathjaxChemistry(): void {
- surround("\\ce{", "}");
+ surround('\\ce{', "}");
}
function onLatex(): void {
diff --git a/ts/editor/mathjax-overlay/MathjaxEditor.svelte b/ts/editor/mathjax-overlay/MathjaxEditor.svelte
index b1657f449..dfd74f6a4 100644
--- a/ts/editor/mathjax-overlay/MathjaxEditor.svelte
+++ b/ts/editor/mathjax-overlay/MathjaxEditor.svelte
@@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "../../lib/ftl";
import { noop } from "../../lib/functional";
import { getPlatformString } from "../../lib/shortcuts";
- import { baseOptions, focusAndCaretAfter, latex } from "../code-mirror";
+ import { baseOptions, focusAndSetCaret, latex } from "../code-mirror";
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
import CodeMirror from "../CodeMirror.svelte";
@@ -33,58 +33,60 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mode: latex,
};
+ /* These are not reactive, but only operate on initialization */
+ export let position: CodeMirrorLib.Position | undefined = undefined;
export let selectAll: boolean;
const dispatch = createEventDispatcher();
let codeMirror = {} as CodeMirrorAPI;
- onMount(() =>
- codeMirror.editor.then((editor) => {
- focusAndCaretAfter(editor);
+ onMount(async () => {
+ const editor = await codeMirror.editor;
- if (selectAll) {
- editor.execCommand("selectAll");
- }
+ focusAndSetCaret(editor, position);
- let direction: "start" | "end" | undefined = undefined;
+ if (selectAll) {
+ editor.execCommand("selectAll");
+ }
- editor.on(
- "keydown",
- (_instance: CodeMirrorLib.Editor, event: KeyboardEvent): void => {
- if (event.key === "ArrowLeft") {
- direction = "start";
- } else if (event.key === "ArrowRight") {
- direction = "end";
- }
- },
- );
+ let direction: "start" | "end" | undefined = undefined;
- editor.on(
- "beforeSelectionChange",
- (
- instance: CodeMirrorLib.Editor,
- obj: CodeMirrorLib.EditorSelectionChange,
- ): void => {
- const { anchor } = obj.ranges[0];
+ editor.on(
+ "keydown",
+ (_instance: CodeMirrorLib.Editor, event: KeyboardEvent): void => {
+ if (event.key === "ArrowLeft") {
+ direction = "start";
+ } else if (event.key === "ArrowRight") {
+ direction = "end";
+ }
+ },
+ );
- if (anchor["hitSide"]) {
- if (instance.getValue().length === 0) {
- if (direction) {
- dispatch(`moveout${direction}`);
- }
- } else if (anchor.line === 0 && anchor.ch === 0) {
- dispatch("moveoutstart");
- } else {
- dispatch("moveoutend");
+ editor.on(
+ "beforeSelectionChange",
+ (
+ instance: CodeMirrorLib.Editor,
+ obj: CodeMirrorLib.EditorSelectionChange,
+ ): void => {
+ const { anchor } = obj.ranges[0];
+
+ if (anchor["hitSide"]) {
+ if (instance.getValue().length === 0) {
+ if (direction) {
+ dispatch(`moveout${direction}`);
}
+ } else if (anchor.line === 0 && anchor.ch === 0) {
+ dispatch("moveoutstart");
+ } else {
+ dispatch("moveoutend");
}
+ }
- direction = undefined;
- },
- );
- }),
- );
+ direction = undefined;
+ },
+ );
+ });
/**
* Escape characters which are technically legal in Mathjax, but confuse HTML.
diff --git a/ts/editor/mathjax-overlay/MathjaxHandle.svelte b/ts/editor/mathjax-overlay/MathjaxHandle.svelte
index a81da07be..6f13d384d 100644
--- a/ts/editor/mathjax-overlay/MathjaxHandle.svelte
+++ b/ts/editor/mathjax-overlay/MathjaxHandle.svelte
@@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->