mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -04:00
Merge branch 'main' into apkg
This commit is contained in:
commit
4c79a1d969
55 changed files with 513 additions and 795 deletions
2
defs.bzl
2
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()
|
||||
|
|
2
ftl/.gitignore
vendored
Normal file
2
ftl/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
usage
|
||||
|
|
@ -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 }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = <html><head/><body><p><span style=" font-weight:600;">Backups</span><br/>Anki will create a backup of your collection each time it is closed.</p></body></html>
|
||||
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
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
1
ftl/usage/.gitignore
vendored
1
ftl/usage/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
desktop-head.json
|
|
@ -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"
|
||||
]
|
|
@ -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;
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 = "<br>".join(log)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="maxVisibleItems">
|
||||
<number>30</number>
|
||||
</property>
|
||||
|
|
|
@ -1207,6 +1207,7 @@ title="{}" {}>{}</button>""".format(
|
|||
advise_restart=not startup,
|
||||
strictly_modal=startup,
|
||||
parent=None if startup else self,
|
||||
force_enable=True,
|
||||
)
|
||||
|
||||
# Cramming
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@echo off
|
||||
anki
|
||||
anki %*
|
||||
pause
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -182,7 +182,7 @@ impl SchedulerService for Backend {
|
|||
}
|
||||
|
||||
fn answer_card(&self, input: pb::CardAnswer) -> Result<pb::OpChanges> {
|
||||
self.with_col(|col| col.answer_card(&input.into()))
|
||||
self.with_col(|col| col.answer_card(&mut input.into()))
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<pb::String> {
|
||||
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(),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<OpOutput<()>> {
|
||||
/// Provided [CardAnswer] has its answer time capped to deck preset.
|
||||
pub fn answer_card(&mut self, answer: &mut CardAnswer) -> Result<OpOutput<()>> {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<FilteredDeck> {
|
|||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<usize> {
|
||||
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)
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>) -> SearchBuilder;
|
||||
/// Concatenates two sets of [Node]s, inserting [Node::Or], and grouping, if appropriate.
|
||||
fn or(self, other: impl Into<SearchBuilder>) -> 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>) -> 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>) -> SearchBuilder;
|
||||
}
|
||||
|
||||
impl<T: Into<Node>> Negated for T {
|
||||
fn negated(self) -> Node {
|
||||
let node: Node = self.into();
|
||||
|
@ -23,6 +36,24 @@ impl<T: Into<Node>> Negated for T {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Into<SearchBuilder>> JoinSearches for T {
|
||||
fn and(self, other: impl Into<SearchBuilder>) -> SearchBuilder {
|
||||
self.into().join_other(other.into(), Node::And, true)
|
||||
}
|
||||
|
||||
fn or(self, other: impl Into<SearchBuilder>) -> SearchBuilder {
|
||||
self.into().join_other(other.into(), Node::Or, true)
|
||||
}
|
||||
|
||||
fn and_flat(self, other: impl Into<SearchBuilder>) -> SearchBuilder {
|
||||
self.into().join_other(other.into(), Node::And, false)
|
||||
}
|
||||
|
||||
fn or_flat(self, other: impl Into<SearchBuilder>) -> SearchBuilder {
|
||||
self.into().join_other(other.into(), Node::Or, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to programmatically build searches.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct SearchBuilder(Vec<Node>);
|
||||
|
@ -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<SearchBuilder>) -> 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<SearchBuilder>) -> 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(", ")
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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<string>;
|
||||
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<Writable<"ltr" | "rtl">>(directionKey);
|
||||
|
||||
$: setOption("direction", $direction);
|
||||
$: setOption("theme", $pageTheme.isDark ? darkTheme : lightTheme);
|
||||
|
||||
let apiPartial: Partial<CodeMirrorAPI>;
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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<OpenCodeMirrorOptions>,
|
||||
options: Partial<OpenCodeMirrorOptions>,
|
||||
): { update: (options: Partial<OpenCodeMirrorOptions>) => void; destroy: () => void } {
|
||||
const editor = CodeMirror.fromTextArea(textarea, configuration);
|
||||
resolve?.(editor);
|
||||
let editor: CodeMirror.EditorFromTextArea | null = null;
|
||||
|
||||
return {
|
||||
update({ configuration }: Partial<OpenCodeMirrorOptions>): void {
|
||||
function update({
|
||||
configuration,
|
||||
resolve,
|
||||
hidden,
|
||||
}: Partial<OpenCodeMirrorOptions>): 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ColorPicker
|
||||
on:input={(event) => {
|
||||
color = setColor(event);
|
||||
bridgeCommand(`lastTextColor:${color}`);
|
||||
bridgeCommand(`lastHighlightColor:${color}`);
|
||||
}}
|
||||
on:change={() => setTextColor()}
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function onMathjaxChemistry(): void {
|
||||
surround("<anki-mathjax focusonmount>\\ce{", "}</anki-mathjax>");
|
||||
surround('<anki-mathjax focusonmount="0,4">\\ce{', "}</anki-mathjax>");
|
||||
}
|
||||
|
||||
function onLatex(): void {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type CodeMirrorLib from "codemirror";
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
|
@ -26,9 +27,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let allow = noop;
|
||||
let unsubscribe = noop;
|
||||
|
||||
function showHandle(image: HTMLImageElement): void {
|
||||
allow = preventResubscription();
|
||||
let selectAll = false;
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
function showHandle(image: HTMLImageElement, pos?: CodeMirrorLib.Position): void {
|
||||
allow = preventResubscription();
|
||||
position = pos;
|
||||
|
||||
/* Setting the activeImage and mathjaxElement to a non-nullish value is
|
||||
* what triggers the Mathjax editor to show */
|
||||
activeImage = image;
|
||||
mathjaxElement = activeImage.closest(Mathjax.tagName)!;
|
||||
|
||||
|
@ -38,8 +45,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
});
|
||||
}
|
||||
|
||||
let selectAll = false;
|
||||
|
||||
function placeHandle(after: boolean): void {
|
||||
editable.focusHandler.flushCaret();
|
||||
|
||||
|
@ -52,6 +57,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
async function resetHandle(): Promise<void> {
|
||||
selectAll = false;
|
||||
position = undefined;
|
||||
|
||||
if (activeImage && mathjaxElement) {
|
||||
unsubscribe();
|
||||
|
@ -72,9 +78,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
async function showAutofocusHandle({
|
||||
detail,
|
||||
}: CustomEvent<HTMLImageElement>): Promise<void> {
|
||||
}: CustomEvent<{
|
||||
image: HTMLImageElement;
|
||||
position?: [number, number];
|
||||
}>): Promise<void> {
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
await resetHandle();
|
||||
showHandle(detail);
|
||||
|
||||
if (detail.position) {
|
||||
const [line, ch] = detail.position;
|
||||
position = { line, ch };
|
||||
}
|
||||
|
||||
showHandle(detail.image, position);
|
||||
}
|
||||
|
||||
async function showSelectAll({
|
||||
|
@ -138,6 +155,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
element={mathjaxElement}
|
||||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
bind:updateSelection
|
||||
on:reset={resetHandle}
|
||||
on:moveoutstart={() => {
|
||||
|
|
|
@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type CodeMirrorLib from "codemirror";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
|
@ -15,7 +16,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let element: Element;
|
||||
export let code: Writable<string>;
|
||||
|
||||
export let selectAll: boolean;
|
||||
export let position: CodeMirrorLib.Position | undefined;
|
||||
|
||||
const acceptShortcut = "Enter";
|
||||
const newlineShortcut = "Shift+Enter";
|
||||
|
@ -40,6 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{newlineShortcut}
|
||||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
on:blur={() => dispatch("reset")}
|
||||
on:moveoutstart
|
||||
on:moveoutend
|
||||
|
|
|
@ -136,7 +136,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
class:hidden
|
||||
on:focusin={() => ($focusedInput = api)}
|
||||
>
|
||||
<CodeMirror {configuration} {code} bind:api={codeMirror} on:change={onChange} />
|
||||
<CodeMirror
|
||||
{configuration}
|
||||
{code}
|
||||
{hidden}
|
||||
bind:api={codeMirror}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -17,5 +17,7 @@ def esbuild(name, **kwargs):
|
|||
"//:release": True,
|
||||
"//conditions:default": False,
|
||||
}),
|
||||
# support Qt 5.14
|
||||
target = "chrome77",
|
||||
**kwargs
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue