Merge branch 'main' into apkg

This commit is contained in:
RumovZ 2022-04-13 09:50:27 +02:00
commit 4c79a1d969
55 changed files with 513 additions and 795 deletions

View file

@ -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
View file

@ -0,0 +1,2 @@
usage

View file

@ -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 }

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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/)

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -1 +0,0 @@
desktop-head.json

View file

@ -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"
]

View file

@ -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;

View file

@ -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__)

View file

@ -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

View file

@ -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:

View file

@ -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"

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

@ -49,6 +49,9 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumContentsLength">
<number>50</number>
</property>
<property name="maxVisibleItems">
<number>30</number>
</property>

View file

@ -1207,6 +1207,7 @@ title="{}" {}>{}</button>""".format(
advise_restart=not startup,
strictly_modal=startup,
parent=None if startup else self,
force_enable=True,
)
# Cramming

View file

@ -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()

View file

@ -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(),
)

View file

@ -1,5 +1,5 @@
@echo off
anki
anki %*
pause

View file

@ -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(

View file

@ -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)
}

View file

@ -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(),

View file

@ -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))

View file

@ -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()
{

View file

@ -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 =

View file

@ -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();

View file

@ -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,

View file

@ -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,

View file

@ -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()

View file

@ -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(())
}
}

View file

@ -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)

View file

@ -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(())
}
}

View file

@ -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"
);
}
}

View file

@ -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,
};

View file

@ -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 {

View file

@ -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(", ")
}

View file

@ -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;

View file

@ -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,
}),

View file

@ -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");

View file

@ -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>

View file

@ -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;
},
};
}

View file

@ -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()}
/>

View file

@ -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 {

View file

@ -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.

View file

@ -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={() => {

View file

@ -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

View file

@ -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">

View file

@ -17,5 +17,7 @@ def esbuild(name, **kwargs):
"//:release": True,
"//conditions:default": False,
}),
# support Qt 5.14
target = "chrome77",
**kwargs
)