diff --git a/defs.bzl b/defs.bzl index ee63a1ed0..dcabd1a34 100644 --- a/defs.bzl +++ b/defs.bzl @@ -10,7 +10,7 @@ load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") load("//python/pyqt:defs.bzl", "install_pyqt") load("@rules_python//python:pip.bzl", "pip_parse") -anki_version = "2.1.50" +anki_version = "2.1.51" def setup_deps(): bazel_skylib_workspace() diff --git a/ftl/.gitignore b/ftl/.gitignore new file mode 100644 index 000000000..738260199 --- /dev/null +++ b/ftl/.gitignore @@ -0,0 +1,2 @@ +usage + diff --git a/ftl/core/custom-study.ftl b/ftl/core/custom-study.ftl index 254338546..06b61ff30 100644 --- a/ftl/core/custom-study.ftl +++ b/ftl/core/custom-study.ftl @@ -29,5 +29,3 @@ custom-study-available-review-cards = Available review cards: { $count } ## DEPRECATED - you do not need to translate these. -custom-study-new-cards-in-deck-over-today = New cards in deck over today limit: { $val } -custom-study-reviews-due-in-deck-over-today = Reviews due in deck over today limit: { $val } diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index d10ab8ae3..f42cf7900 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -122,28 +122,27 @@ deck-config-new-gather-priority-random-notes = Random notes deck-config-new-gather-priority-random-cards = Random cards deck-config-new-card-sort-order = New card sort order deck-config-new-card-sort-order-tooltip-2 = - `Card template`: Displays cards in card template order. If you have sibling burying - disabled, this will ensure all front->back cards are seen before any back->front cards. + `Card type`: Displays cards in order of card type number. If you have sibling burying + disabled, this will ensure all front→back cards are seen before any back→front cards. + This is useful to have all cards of the same note shown in the same session, but not + too close to one another. `Order gathered`: Shows cards exactly as they were gathered. If sibling burying is disabled, this will typically result in all cards of a note being seen one after the other. - `Card template, then random`: Like `Card template`, but shuffles the cards of each - template. When combined with an ascending position gather order, this can be used to show - the oldest cards in a random order for example. + `Card type, then random`: Like `Card type`, but shuffles the cards of each card + type number. If you use `Ascending position` to gather the oldest cards, you could use + this setting to see those cards in a random order, but still ensure cards of the same + note do not end up too close to one another. - `Random note, then card template`: Picks notes at random, then shows all of their siblings + `Random note, then card type`: Picks notes at random, then shows all of their siblings in order. `Random`: Fully shuffles the gathered cards. -deck-config-sort-order-card-template-then-lowest-position = Card template, then ascending position -deck-config-sort-order-card-template-then-highest-position = Card template, then descending position -deck-config-sort-order-card-template-then-random = Card template, then random -deck-config-sort-order-random-note-then-template = Random note, then card template -deck-config-sort-order-lowest-position = Ascending position -deck-config-sort-order-highest-position = Descending position +deck-config-sort-order-card-template-then-random = Card type, then random +deck-config-sort-order-random-note-then-template = Random note, then card type deck-config-sort-order-random = Random -deck-config-sort-order-template-then-gather = Card template +deck-config-sort-order-template-then-gather = Card type deck-config-sort-order-gather = Order gathered deck-config-new-review-priority = New/review order deck-config-new-review-priority-tooltip = When to show new cards in relation to review cards. @@ -171,6 +170,7 @@ deck-config-sort-order-ascending-intervals = Ascending intervals deck-config-sort-order-descending-intervals = Descending intervals deck-config-sort-order-ascending-ease = Ascending ease deck-config-sort-order-descending-ease = Descending ease +deck-config-sort-order-relative-overdueness = Relative overdueness deck-config-display-order-will-use-current-deck = Anki will use the display order from the deck you select to study, and not any subdecks it may have. @@ -273,15 +273,3 @@ deck-config-which-deck = Which deck would you like? ## NO NEED TO TRANSLATE. These strings have been replaced with new versions, and will be removed in the future. -deck-config-new-card-sort-order-tooltip = - How cards are sorted after they have been gathered. By default, Anki sorts - by card template first, to avoid multiple cards of the same note from being - shown in succession. -deck-config-new-gather-priority-tooltip = - `Deck`: gathers cards from each subdeck in order, and stops when the - limit of the selected deck has been exceeded. This is faster, and allows you - to prioritize subdecks that are closer to the top. - - `Position`: gathers cards from all decks before they are sorted. This - ensures cards appear in strict position (due #) order, even if the parent limit is - not high enough to see cards from all decks. diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index f91dface4..df5d391db 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -60,5 +60,3 @@ editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will no ## You don't need to translate these strings, as they will be replaced with different ones soon. editing-html-editor = HTML Editor -editing-set-text-color = Set text color -editing-set-text-highlight-color = Set text highlight color diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index 2d4523204..76e3132a0 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -1,19 +1,15 @@ preferences-automatically-sync-on-profile-openclose = Automatically sync on profile open/close preferences-backups = Backups -preferences-backups2 = backups -preferences-backupsanki-will-create-a-backup-of =

Backups
Anki will create a backup of your collection each time it is closed.

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