Merge branch 'main' into Feat/evaluate-FSRS-with-time-series-split

This commit is contained in:
Jarrett Ye 2025-05-27 16:20:39 +08:00 committed by GitHub
commit 60f4017bc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 830 additions and 314 deletions

View file

@ -0,0 +1,2 @@
- To build and check the project, use ./check in the root folder (or check.bat on Windows)
- This will format files, then run lints and unit tests.

7
.cursor/rules/i18n.md Normal file
View file

@ -0,0 +1,7 @@
- We use the fluent system+code generation for translation.
- New strings should be added to rslib/core/. Ask for the appropriate file if you're not sure.
- Assuming a string addons-you-have-count has been added to addons.ftl, that string is accessible in our different languages as follows:
- Python: from aqt.utils import tr; msg = tr.addons_you_have_count(count=3)
- TypeScript: import * as tr from "@generated/ftl"; tr.addonsYouHaveCount({count: 3})
- Rust: collection.tr.addons_you_have_count(3)
- In Qt .ui files, strings that are marked as translatable will automatically use the registered ftl strings. So a QLabel with a title 'addons_you_have_count' that is marked as translatable will automatically use the translation defined in our addons.ftl file.

View file

@ -23,7 +23,7 @@
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingModuleSource": "none"
},
"rust-analyzer.checkOnSave.allTargets": false,
"rust-analyzer.check.allTargets": false,
"rust-analyzer.files.excludeDirs": [".bazel", "node_modules"],
"rust-analyzer.procMacro.enable": true,
// this formats 'use' blocks in a nicer way, but requires you to run

View file

@ -201,7 +201,7 @@ Dongjin Ouyang <1113117424@qq.com>
Sawan Sunar <sawansunar24072002@gmail.com>
hideo aoyama <https://github.com/boukendesho>
Ross Brown <rbrownwsws@googlemail.com>
🦙 <github.com/iamllama>
🦙 <gh@siid.sh>
Lukas Sommer <sommerluk@gmail.com>
Luca Auer <lolle2000.la@gmail.com>
Niclas Heinz <nheinz@hpost.net>
@ -224,6 +224,12 @@ rreemmii-dev <https://github.com/rreemmii-dev>
babofitos <https://github.com/babofitos>
Jonathan Schoreels <https://github.com/JSchoreels>
JL710
Matt Brubeck <mbrubeck@limpet.net>
Yaoliang Chen <yaoliang.ch@gmail.com>
KolbyML <https://github.com/KolbyML>
Adnane Taghi <dev@soleuniverse.me>
Spiritual Father <https://github.com/spiritualfather>
Emmanuel Ferdman <https://github.com/emmanuel-ferdman>
********************

2
Cargo.lock generated
View file

@ -2295,7 +2295,7 @@ dependencies = [
[[package]]
name = "fsrs"
version = "4.0.0"
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=b6b1ab87aaf8e25b72ad75ff763b6cd609f12640#b6b1ab87aaf8e25b72ad75ff763b6cd609f12640"
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=33ec3ee4d5d73e704633469cf5bf1a42e620a524#33ec3ee4d5d73e704633469cf5bf1a42e620a524"
dependencies = [
"burn",
"itertools 0.14.0",

View file

@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs]
# version = "3.0.0"
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
rev = "b6b1ab87aaf8e25b72ad75ff763b6cd609f12640"
rev = "33ec3ee4d5d73e704633469cf5bf1a42e620a524"
# path = "../open-spaced-repetition/fsrs-rs"
[workspace.dependencies]

View file

@ -246,7 +246,7 @@ pub fn check_minilints(build: &mut Build) -> Result<()> {
let files = inputs![
glob![
"**/*.{py,rs,ts,svelte,mjs}",
"**/*.{py,rs,ts,svelte,mjs,md}",
"{node_modules,qt/bundle/PyOxidizer,ts/.svelte-kit}/**"
],
"Cargo.lock"

View file

@ -8,7 +8,7 @@ mentioned there no longer apply:
https://forums.ankiweb.net/t/guide-how-to-build-and-run-anki-from-source-with-xubuntu-20-04/12865
You can see a full list of buildtime and runtime requirements by looking at the
[Dockerfiles](../.buildkite/linux/docker/Dockerfile.amd64) used to build the
[Dockerfile](../.buildkite/linux/docker/Dockerfile) used to build the
official releases.
**Ensure some basic tools are installed**:

View file

@ -1,4 +1,4 @@
FROM rust:1.83.0-alpine3.20 AS builder
FROM rust:1.85.0-alpine3.20 AS builder
ARG ANKI_VERSION

View file

@ -1,4 +1,4 @@
FROM rust:1.83.0 AS builder
FROM rust:1.85.0 AS builder
ARG ANKI_VERSION

@ -1 +1 @@
Subproject commit 376ae99eb47eae3c5e6298150162715423bc2e4e
Subproject commit ca04132a8f82296f3e0ea22b74bb4221e1d11d3f

View file

@ -470,11 +470,12 @@ deck-config-compute-optimal-retention-tooltip4 =
willing to invest more study time to achieve it. Setting your desired retention lower than the minimum
is not recommended, as it will lead to a higher workload, because of the high forgetting rate.
deck-config-please-save-your-changes-first = Please save your changes first.
deck-config-a-100-day-interval =
{ $days ->
[one] A 100 day interval will become { $days } day.
*[other] A 100 day interval will become { $days } days.
}
deck-config-workload-factor-change = Approximate workload: {$factor}x
(compared to {$previousDR}% desired retention)
deck-config-workload-factor-unchanged = The higher this value, the more frequently cards will be shown to you.
deck-config-desired-retention-too-low = Your desired retention is very low, which can lead to very long intervals.
deck-config-desired-retention-too-high = Your desired retention is very high, which can lead to very short intervals.
deck-config-percent-of-reviews =
{ $reviews ->
[one] { $pct }% of { $reviews } review
@ -484,7 +485,7 @@ deck-config-percent-input = { $pct }%
deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
deck-config-fsrs-must-be-enabled = FSRS must be enabled first.
deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal.
deck-config-fsrs-params-no-reviews = No reviews found. Please check that this preset is assigned to all decks you want to optimize (including subdecks) and try again.
deck-config-fsrs-params-no-reviews = No reviews found. Make sure this preset is assigned to all decks (including subdecks) that you want to optimize, and try again.
deck-config-wait-for-audio = Wait for audio
deck-config-show-reminder = Show Reminder
@ -512,6 +513,12 @@ deck-config-fsrs-simulator-radio-memorized = Memorized
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
deck-config-a-100-day-interval =
{ $days ->
[one] A 100 day interval will become { $days } day.
*[other] A 100 day interval will become { $days } days.
}
deck-config-fsrs-simulator-y-axis-title-time = Review Time/Day
deck-config-fsrs-simulator-y-axis-title-count = Review Count/Day
deck-config-fsrs-simulator-y-axis-title-memorized = Memorized Total

View file

@ -15,6 +15,7 @@ importing-colon = Colon
importing-comma = Comma
importing-empty-first-field = Empty first field: { $val }
importing-field-separator = Field separator
importing-field-separator-guessed = Field separator (guessed)
importing-field-mapping = Field mapping
importing-field-of-file-is = Field <b>{ $val }</b> of file is:
importing-fields-separated-by = Fields separated by: { $val }
@ -217,6 +218,9 @@ importing-field-separator-help =
Please note that if this character appears in any field itself, the field has to be
quoted accordingly to the CSV standard. Spreadsheet programs like LibreOffice will
do this automatically.
It cannot be changed if the text file forces use of a specific separator via a file header.
If a file header is not present, Anki will try to guess what the separator is.
importing-allow-html-in-fields-help =
Enable this if the file contains HTML formatting. E.g. if the file contains the string
'&lt;br&gt;', it will appear as a line break on your card. On the other hand, with this

View file

@ -83,6 +83,15 @@ preferences-ankiweb-intro = AnkiWeb is a free service that lets you keep your fl
preferences-ankihub-intro = AnkiHub provides collaborative deck editing and additional study tools. A paid subscription is required to access certain features.
preferences-third-party-description = Third-party services are unaffiliated with and not endorsed by Anki. Use of these services may require payment.
## URL scheme related
preferences-url-schemes = URL Schemes
preferences-url-scheme-prompt = Allowed URL Schemes (space-separated):
preferences-url-scheme-warning = Blocked attempt to open `{ $link }`, which may be a security issue.
If you trust the deck author and wish to proceed, you can add `{ $scheme }` to your allowed URL Schemes.
preferences-url-scheme-allow-once = Allow Once
preferences-url-scheme-always-allow = Always Allow
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
preferences-basic = Basic

View file

@ -229,6 +229,7 @@ statistics-stability-day-single =
# hour range, eg "From 14:00-15:00"
statistics-hours-range = From { $hourStart }:00~{ $hourEnd }:00
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
statistics-hours-correct-info = → (not 'Again')
# the emoji depicts the graph displaying this number
statistics-hours-reviews = 📊 { $reviews } reviews
# the emoji depicts the graph displaying this number

View file

@ -50,10 +50,11 @@ sync-account-required =
A free account is required to keep your collection synchronized. Please <a href="{ $link }">sign up</a> for an account, then enter your details below.
sync-sanity-check-failed = Please use the Check Database function, then sync again. If problems persist, please force a one-way sync in the preferences screen.
sync-clock-off = Unable to sync - your clock is not set to the correct time.
# “details” expands to a string such as “300.14 MB > 300.00 MB”
sync-upload-too-large =
Your collection file is too large to send to AnkiWeb. You can reduce its
size by removing any unwanted decks (optionally exporting them first), and
then using Check Database to shrink the file size down. ({ $details })
Your collection file is too large to send to AnkiWeb. You can reduce its size by removing any unwanted decks (optionally exporting them first), and then using Check Database to shrink the file size down.
{ $details } (uncompressed)
sync-sign-in = Sign in
sync-ankihub-dialog-heading = AnkiHub Login
sync-ankihub-username-label = Username or Email:

@ -1 +1 @@
Subproject commit 393bacec35703f91520e4d2feec37ed6953114f4
Subproject commit f35acabb46dc9197a62c47eb7f2ca062628b1d94

View file

@ -3,7 +3,7 @@ qt-misc-addons = Add-ons
qt-misc-all-cards-notes-and-media-for = All cards, notes, and media for this profile will be deleted. Are you sure?
qt-misc-all-cards-notes-and-media-for2 = All cards, notes, and media for the profile "{ $name }" will be deleted. Are you sure?
qt-misc-anki-updatedanki-has-been-released = <h1>Anki Updated</h1>Anki { $val } has been released.<br><br>
qt-misc-automatic-syncing-and-backups-have-been = Automatic syncing and backups have been disabled while restoring. To enable them again, close the profile or restart Anki.
qt-misc-automatic-syncing-and-backups-have-been = Backup successfully restored. Automatic syncing and backups have been disabled for now. To enable them again, close the profile or restart Anki.
qt-misc-back-side-only = Back Side Only
qt-misc-backing-up = Backing Up...
qt-misc-browse = Browse

View file

@ -37,8 +37,8 @@
"cross-env": "^7.0.2",
"diff": "^5.0.0",
"dprint": "^0.47.2",
"esbuild": "^0.25.0",
"esbuild-sass-plugin": "^2",
"esbuild": "^0.25.3",
"esbuild-sass-plugin": "^3.3.1",
"esbuild-svelte": "^0.9.2",
"eslint": "^8.44.0",
"eslint-plugin-compat": "^4.1.4",
@ -56,7 +56,7 @@
"tslib": "^2.0.3",
"tsx": "^3.12.0",
"typescript": "^5.0.4",
"vite": "5.4.18",
"vite": "5.4.19",
"vitest": "^2"
},
"dependencies": {

View file

@ -25,6 +25,8 @@ service DeckConfigService {
returns (collection.OpChanges);
rpc GetIgnoredBeforeCount(GetIgnoredBeforeCountRequest)
returns (GetIgnoredBeforeCountResponse);
rpc GetRetentionWorkload(GetRetentionWorkloadRequest)
returns (GetRetentionWorkloadResponse);
}
// Implicitly includes any of the above methods that are not listed in the
@ -35,6 +37,17 @@ message DeckConfigId {
int64 dcid = 1;
}
message GetRetentionWorkloadRequest {
repeated float w = 1;
string search = 2;
float before = 3;
float after = 4;
}
message GetRetentionWorkloadResponse {
float factor = 1;
}
message GetIgnoredBeforeCountRequest {
string ignore_revlogs_before_date = 1;
string search = 2;

View file

@ -48,6 +48,7 @@ class Card(DeprecatedNamesMixin):
type: CardType
memory_state: FSRSMemoryState | None
desired_retention: float | None
decay: float | None
def __init__(
self,
@ -101,6 +102,7 @@ class Card(DeprecatedNamesMixin):
self.desired_retention = (
card.desired_retention if card.HasField("desired_retention") else None
)
self.decay = card.decay if card.HasField("decay") else None
def _to_backend_card(self) -> cards_pb2.Card:
# mtime & usn are set by backend
@ -124,6 +126,7 @@ class Card(DeprecatedNamesMixin):
custom_data=self.custom_data,
memory_state=self.memory_state,
desired_retention=self.desired_retention,
decay=self.decay,
)
@deprecated(info="please use col.update_card()")

View file

@ -136,9 +136,9 @@ flask==3.0.3 \
# via
# -r requirements.aqt.in
# flask-cors
flask-cors==5.0.0 \
--hash=sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef \
--hash=sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc
flask-cors==6.0.0 \
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
# via -r requirements.aqt.in
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
@ -397,7 +397,9 @@ waitress==3.0.1 \
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via flask
# via
# flask
# flask-cors
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49

View file

@ -183,9 +183,9 @@ flask==3.0.3 \
# -r requirements.aqt.in
# flask-cors
# types-flask-cors
flask-cors==5.0.0 \
--hash=sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef \
--hash=sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc
flask-cors==6.0.0 \
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
# via -r requirements.aqt.in
fluent-syntax==0.19.0 \
--hash=sha256:920326d7f46864b9758f0044e9968e3112198bc826acee16ddd8f11d359004fd \
@ -620,7 +620,9 @@ websocket-client==1.8.0 \
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via flask
# via
# flask
# flask-cors
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49

View file

@ -87,7 +87,7 @@ except AttributeError:
appVersion = _version
appWebsite = "https://apps.ankiweb.net/"
appWebsiteDownloadSection = "https://apps.ankiweb.net/#download"
appDonate = "https://apps.ankiweb.net/support/"
appDonate = "https://docs.ankiweb.net/contrib.html"
appShared = "https://ankiweb.net/shared/"
appUpdate = "https://ankiweb.net/update/desktop"
appHelpSite = HELP_SITE

View file

@ -221,9 +221,12 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Yuki",
"🦙 (siid)",
"Mukunda Madhav Dey",
"Adnane Taghi",
"Anon_0000",
)
)
allusers = [user.replace(" ", "&nbsp;") for user in allusers]
abouttext += "<p>" + tr.about_written_by_damien_elmes_with_patches(
cont=", ".join(allusers) + f", {tr.about_and_others()}"
)

View file

@ -397,6 +397,7 @@ class Browser(QMainWindow):
add_ellipsis_to_action_label(f.actionCopy)
add_ellipsis_to_action_label(f.action_forget)
add_ellipsis_to_action_label(f.action_grade_now)
def _editor_web_view(self) -> EditorWebView:
assert self.editor is not None
@ -1243,11 +1244,13 @@ class Browser(QMainWindow):
self._line_edit().selectAll()
def onNote(self) -> None:
assert self.editor is not None
assert self.editor.web is not None
def cb():
assert self.editor is not None and self.editor.web is not None
self.editor.web.setFocus()
self.editor.loadNote(focusTo=0)
self.editor.web.setFocus()
self.editor.loadNote(focusTo=0)
assert self.editor is not None
self.editor.call_after_note_saved(cb)
def onCardList(self) -> None:
self.form.tableView.setFocus()

View file

@ -15,7 +15,6 @@ import anki.cards
import aqt
import aqt.forms
from aqt import gui_hooks
from aqt.profiles import ProfileManager
from aqt.qt import *
from aqt.utils import (
disable_help_button,
@ -80,7 +79,7 @@ class DebugConsole(QDialog):
self._log.setFont(font)
def _setup_scripts(self) -> None:
self._dir = ProfileManager.get_created_base_folder(None).joinpath(SCRIPT_FOLDER)
self._dir = Path(aqt.mw.pm.base).joinpath(SCRIPT_FOLDER)
self._dir.mkdir(exist_ok=True)
self._script.addItem(UNSAVED_SCRIPT)
self._script.addItems(os.listdir(self._dir))

View file

@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="currentIndex">
<number>0</number>
@ -78,7 +78,7 @@
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
@ -260,7 +260,7 @@
<item>
<spacer name="verticalSpacer_9">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -451,6 +451,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="url_schemes">
<property name="text">
<string>preferences_url_schemes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -466,7 +473,7 @@
<item>
<spacer name="verticalSpacer_12">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -518,10 +525,10 @@
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -614,10 +621,10 @@
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -739,7 +746,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -827,7 +834,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -840,7 +847,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -918,10 +925,10 @@
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -953,7 +960,7 @@
<item row="1" column="3">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1020,7 +1027,7 @@
<item row="1" column="1">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1035,10 +1042,10 @@
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1080,7 +1087,7 @@
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1128,10 +1135,10 @@
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
<enum>QSizePolicy::Policy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1207,7 +1214,7 @@
<item>
<spacer name="verticalspacer_13">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1227,17 +1234,17 @@
<string>preferences_some_settings_will_take_effect_after</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Help</set>
<set>QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Help</set>
</property>
</widget>
</item>
@ -1266,6 +1273,7 @@
<tabstop>showEstimates</tabstop>
<tabstop>spacebar_rates_card</tabstop>
<tabstop>render_latex</tabstop>
<tabstop>url_schemes</tabstop>
<tabstop>pastePNG</tabstop>
<tabstop>paste_strips_formatting</tabstop>
<tabstop>useCurrent</tabstop>

View file

@ -872,6 +872,9 @@ class AnkiQt(QMainWindow):
if changes.mtime:
self.toolbar.update_sync_status()
if changes.notetype:
self.col.models._clear_cache()
def on_focus_did_change(
self, new_focus: QWidget | None, _old: QWidget | None
) -> None:

View file

@ -659,6 +659,7 @@ exposed_backend_list = [
"simulate_fsrs_review",
# DeckConfigService
"get_ignored_before_count",
"get_retention_workload",
]

View file

@ -30,6 +30,7 @@ from __future__ import annotations
import inspect
import json
import os
import platform
import select
import socket
import subprocess
@ -40,7 +41,7 @@ import time
from queue import Empty, Full, Queue
from shutil import which
from anki.utils import is_win
from anki.utils import is_mac, is_win
class MPVError(Exception):
@ -92,6 +93,9 @@ class MPVBase:
if is_win:
default_argv += ["--af-add=lavfi=[apad=pad_dur=0.150]"]
if not is_mac or platform.machine() != "arm64":
# our arm64 mpv build doesn't support this option (compiled out)
default_argv += ["--no-ytdl"]
def __init__(self, window_id=None, debug=False):
self.window_id = window_id

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import html
from collections.abc import Sequence
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
@ -22,7 +23,7 @@ def remove_decks(
lambda out: tooltip(
tr.browsing_cards_deleted_with_deckname(
count=out.count,
deck_name=deck_name,
deck_name=html.escape(deck_name),
),
parent=parent,
)

View file

@ -20,9 +20,11 @@ from aqt.profiles import VideoDriver
from aqt.qt import *
from aqt.sync import sync_login
from aqt.theme import Theme
from aqt.url_schemes import show_url_schemes_dialog
from aqt.utils import (
HelpPage,
add_close_shortcut,
add_ellipsis_to_action_label,
askUser,
disable_help_button,
is_win,
@ -152,6 +154,9 @@ class Preferences(QDialog):
form.monthly_backups.setValue(self.prefs.backups.monthly)
form.minutes_between_backups.setValue(self.prefs.backups.minimum_interval_mins)
add_ellipsis_to_action_label(self.form.url_schemes)
qconnect(self.form.url_schemes.clicked, show_url_schemes_dialog)
def update_collection(self, on_done: Callable[[], None]) -> None:
form = self.form

View file

@ -744,3 +744,17 @@ create table if not exists profiles
def ankihub_username(self) -> str | None:
return self.profile.get("thirdPartyAnkiHubUsername")
def allowed_url_schemes(self) -> list[str]:
return self.profile.get("allowedUrlSchemes", [])
def set_allowed_url_schemes(self, schemes: list[str]) -> None:
self.profile["allowedUrlSchemes"] = schemes
def always_allow_scheme(self, scheme: str) -> None:
schemes = self.allowed_url_schemes()
if scheme not in schemes:
schemes.append(scheme)
self.set_allowed_url_schemes(schemes)

View file

@ -551,9 +551,11 @@ class Reviewer:
def after_answer(changes: OpChanges) -> None:
if gui_hooks.reviewer_did_answer_card.count() > 0:
self.card.load()
# v3 scheduler doesn't report this
suspended = self.card is not None and self.card.queue < 0
self._after_answering(ease)
if sched.state_is_leech(answer.new_state):
self.onLeech()
self.onLeech(suspended)
self.state = "transition"
answer_card(parent=self.mw, answer=answer).success(
@ -949,11 +951,10 @@ timerStopped = false;
# Leeches
##########################################################################
def onLeech(self, card: Card | None = None) -> None:
def onLeech(self, suspended: bool = False) -> None:
# for now
s = tr.studying_card_was_a_leech()
# v3 scheduler doesn't report this
if card and card.queue < 0:
if suspended:
s += f" {tr.studying_it_has_been_suspended()}"
tooltip(s)

View file

@ -394,6 +394,7 @@ class SimpleMpvPlayer(SimpleProcessPlayer, VideoPlayer):
"--keep-open=no",
"--input-media-keys=no",
"--autoload-files=no",
"--no-ytdl",
]
)

71
qt/aqt/url_schemes.py Normal file
View file

@ -0,0 +1,71 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from markdown import markdown
from aqt.qt import QMessageBox, Qt, QUrl
from aqt.utils import MessageBox, getText, openLink, tr
def show_url_schemes_dialog() -> None:
from aqt import mw
default = " ".join(mw.pm.allowed_url_schemes())
schemes, ok = getText(
prompt=tr.preferences_url_scheme_prompt(),
title=tr.preferences_url_schemes(),
default=default,
)
if ok:
mw.pm.set_allowed_url_schemes(schemes.split(" "))
mw.pm.save()
def is_supported_scheme(url: QUrl) -> bool:
from aqt import mw
scheme = url.scheme().lower()
allowed_schemes = mw.pm.allowed_url_schemes()
return scheme in allowed_schemes or scheme in ["http", "https"]
def always_allow_scheme(url: QUrl) -> None:
from aqt import mw
scheme = url.scheme().lower()
mw.pm.always_allow_scheme(scheme)
def open_url_if_supported_scheme(url: QUrl) -> None:
from aqt import mw
if is_supported_scheme(url):
openLink(url)
else:
def on_button(idx: int) -> None:
if idx == 0:
openLink(url)
elif idx == 1:
always_allow_scheme(url)
openLink(url)
msg = markdown(
tr.preferences_url_scheme_warning(link=url.toString(), scheme=url.scheme())
)
MessageBox(
msg,
buttons=[
tr.preferences_url_scheme_allow_once(),
tr.preferences_url_scheme_always_allow(),
(tr.actions_cancel(), QMessageBox.ButtonRole.RejectRole),
],
parent=mw,
callback=on_button,
textFormat=Qt.TextFormat.RichText,
default_button=2,
icon=QMessageBox.Icon.Warning,
)

View file

@ -930,7 +930,7 @@ def openFolder(path: str) -> None:
subprocess.run(["explorer", f"file://{path}"], check=False)
else:
with no_bundled_libs():
QDesktopServices.openUrl(QUrl(f"file://{path}"))
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def show_in_folder(path: str) -> None:
@ -947,7 +947,7 @@ def show_in_folder(path: str) -> None:
else:
# Just open the file in any other platform
with no_bundled_libs():
QDesktopServices.openUrl(QUrl(f"file://{path}"))
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def _show_in_folder_win32(path: str) -> None:
@ -1188,7 +1188,7 @@ def disallow_full_screen() -> bool:
)
def add_ellipsis_to_action_label(*actions: QAction) -> None:
def add_ellipsis_to_action_label(*actions: QAction | QPushButton) -> None:
"""Pass actions to add '...' to their labels, indicating that more input is
required before they can be performed.

View file

@ -266,7 +266,9 @@ class AnkiWebPage(QWebEnginePage):
print("onclick handler needs to return false")
return False
# load all other links in browser
openLink(url)
from aqt.url_schemes import open_url_if_supported_scheme
open_url_if_supported_scheme(url)
return False
def _onCmd(self, str: str) -> Any:

View file

@ -38,7 +38,7 @@ with open(input_path, "r") as f:
if "fill" in data:
data = re.sub(r"fill=\"#.+?\"", f'fill="{color[mode]}"', data)
else:
data = re.sub(r"<svg", f'<svg fill="{color[mode]}"', data, 1)
data = re.sub(r"<svg", f'<svg fill="{color[mode]}"', data, count=1)
with open(filename, "w") as f:
f.write(data)

View file

@ -96,6 +96,40 @@ impl crate::services::DeckConfigService for Collection {
total: guard.cards.try_into().unwrap_or(0),
})
}
fn get_retention_workload(
&mut self,
input: anki_proto::deck_config::GetRetentionWorkloadRequest,
) -> Result<anki_proto::deck_config::GetRetentionWorkloadResponse> {
const LEARN_SPAN: usize = 1000;
let guard =
self.search_cards_into_table(&input.search, crate::search::SortMode::NoOrder)?;
let (pass_cost, fail_cost, learn_cost) = guard.col.storage.get_costs_for_retention()?;
let before = fsrs::expected_workload(
&input.w,
input.before,
LEARN_SPAN,
pass_cost,
fail_cost,
0.,
input.before,
)? + learn_cost;
let after = fsrs::expected_workload(
&input.w,
input.after,
LEARN_SPAN,
pass_cost,
fail_cost,
0.,
input.after,
)? + learn_cost;
Ok(anki_proto::deck_config::GetRetentionWorkloadResponse {
factor: after / before,
})
}
}
impl From<DeckConfig> for anki_proto::deck_config::DeckConfig {

View file

@ -74,6 +74,11 @@ pub fn get_image_cloze_data(text: &str) -> String {
result.push_str(&format!("data-top=\"{}\" ", property.value));
}
}
"angle" => {
if !property.value.is_empty() {
result.push_str(&format!("data-angle=\"{}\" ", property.value));
}
}
"width" => {
if !is_empty_or_zero(&property.value) {
result.push_str(&format!("data-width=\"{}\" ", property.value));

View file

@ -71,7 +71,7 @@ impl CardStateUpdater {
// Decrease reps by 1 to get correct seed for fuzz.
// If the fuzz calculation changes, this will break.
let last_ivl_with_fuzz = self.learning_ivl_with_fuzz(
get_fuzz_seed_for_id_and_reps(self.card.id, self.card.reps - 1),
get_fuzz_seed_for_id_and_reps(self.card.id, self.card.reps.wrapping_sub(1)),
last_ivl,
);
let last_answered_time = due as i64 - last_ivl_with_fuzz as i64;

View file

@ -368,8 +368,6 @@ impl Collection {
}))
),
)?;
} else if card.queue == CardQueue::Suspended {
invalid_input!("Can't answer suspended cards");
}
Ok(())

View file

@ -314,17 +314,18 @@ pub(crate) fn reviews_for_fsrs(
if entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0 {
continue;
}
// For incomplete review histories, initial memory state is based on the first
// user-graded review after the cutoff date with interval >= 1d.
let within_cutoff = entry.id.0 > ignore_revlogs_before.0;
let user_graded = matches!(entry.button_chosen, 1..=4);
if user_graded && within_cutoff {
let interday = entry.interval >= 1 || entry.interval <= -86400;
if user_graded && within_cutoff && interday {
first_user_grade_idx = Some(index);
}
if user_graded && entry.review_kind == RevlogReviewKind::Learning {
first_of_last_learn_entries = Some(index);
revlogs_complete = true;
} else if first_of_last_learn_entries.is_some() {
break;
} else if matches!(
(entry.review_kind, entry.ease_factor),
(RevlogReviewKind::Manual, 0)
@ -344,6 +345,10 @@ pub(crate) fn reviews_for_fsrs(
} else {
return None;
}
// Previous versions of Anki didn't add a revlog entry when the card was
// reset.
} else if first_of_last_learn_entries.is_some() {
break;
}
}
if training {
@ -476,6 +481,7 @@ pub(crate) mod tests {
review_kind,
id: days_ago_ms(days_ago).into(),
button_chosen: 3,
interval: 1,
..Default::default()
}
}
@ -710,6 +716,28 @@ pub(crate) mod tests {
assert_eq!(convert_ignore_before(revlogs, true, days_ago_ms(9)), None);
}
#[test]
fn skip_initial_relearning_steps() {
let revlogs = &[
revlog(RevlogReviewKind::Review, 10),
RevlogEntry {
button_chosen: 1, // Again
interval: -600,
..revlog(RevlogReviewKind::Review, 8)
},
revlog(RevlogReviewKind::Relearning, 8),
revlog(RevlogReviewKind::Review, 6),
];
// | = Ignore before
// A = Again
// X = Relearning
// R | A X R
assert_eq!(
convert_ignore_before(revlogs, false, days_ago_ms(9)),
fsrs_items!([review(0)], [review(0), review(2)])
);
}
#[test]
fn ignore_before_date_between_learning_steps_when_reviewing() {
let revlogs = &[

View file

@ -32,12 +32,14 @@ impl Card {
force_reset: bool,
) {
let new_due = (today + days_from_today) as i32;
let new_interval =
if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
days_from_today
} else {
self.interval
};
let fsrs_enabled = self.memory_state.is_some();
let new_interval = if fsrs_enabled {
self.interval.saturating_add_signed(new_due - self.due)
} else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) {
days_from_today
} else {
self.interval
};
let ease_factor = (ease_factor * 1000.0).round() as u16;
self.schedule_as_review(new_interval, new_due, ease_factor);

View file

@ -583,8 +583,10 @@ impl SqlWriter<'_> {
}
fn write_single_field(&mut self, field_name: &str, val: &str) -> Result<()> {
let field_indicies_by_notetype =
self.num_fields_and_fields_indices_by_notetype(field_name)?;
let field_indicies_by_notetype = self.num_fields_and_fields_indices_by_notetype(
field_name,
matches!(val, "*" | "_*" | "*_"),
)?;
if field_indicies_by_notetype.is_empty() {
write!(self.sql, "false").unwrap();
return Ok(());
@ -630,6 +632,7 @@ impl SqlWriter<'_> {
fn num_fields_and_fields_indices_by_notetype(
&mut self,
field_name: &str,
test_for_nonempty: bool,
) -> Result<Vec<FieldQualifiedSearchContext>> {
let matches_glob = glob_matcher(field_name);
@ -640,7 +643,7 @@ impl SqlWriter<'_> {
.iter()
.filter(|&field| matches_glob(&field.name))
.map(|field| field.ord.unwrap_or_default())
.collect_ranges();
.collect_ranges(!test_for_nonempty);
if !matched_fields.is_empty() {
field_map.push(FieldQualifiedSearchContext {
ntid: nt.id,
@ -697,7 +700,7 @@ impl SqlWriter<'_> {
}
(!field.config.exclude_from_search).then_some(ord)
})
.collect_ranges();
.collect_ranges(true);
if !matched_fields.is_empty() {
field_map.push(UnqualifiedSearchContext {
ntid: nt.id,
@ -899,7 +902,7 @@ impl RequiredTable {
/// contiguous numbers.
trait CollectRanges {
type Item;
fn collect_ranges(self) -> Vec<Range<Self::Item>>;
fn collect_ranges(self, join: bool) -> Vec<Range<Self::Item>>;
}
impl<
@ -909,7 +912,7 @@ impl<
{
type Item = Idx;
fn collect_ranges(self) -> Vec<Range<Self::Item>> {
fn collect_ranges(self, join: bool) -> Vec<Range<Self::Item>> {
let mut result = Vec::new();
let mut iter = self.into_iter();
let next = iter.next();
@ -920,7 +923,7 @@ impl<
let mut end = next.unwrap();
for i in iter {
if i == end + 1.into() {
if join && i == end + 1.into() {
end = end + 1.into();
} else {
result.push(start..end + 1.into());
@ -1334,7 +1337,8 @@ c.odue != 0 then c.odue else c.due end) != {days}) or (c.queue in (1,4) and
#[allow(clippy::single_range_in_vec_init)]
#[test]
fn ranges() {
assert_eq!([1, 2, 3].collect_ranges(), [1..4]);
assert_eq!([1, 3, 4].collect_ranges(), [1..2, 3..5]);
assert_eq!([1, 2, 3].collect_ranges(true), [1..4]);
assert_eq!([1, 3, 4].collect_ranges(true), [1..2, 3..5]);
assert_eq!([1, 2, 5, 6].collect_ranges(false), [1..2, 2..3, 5..6, 6..7]);
}
}

View file

@ -30,19 +30,21 @@ impl Collection {
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
let timing = self.timing_today()?;
let days_elapsed = self
let seconds_elapsed = self
.storage
.time_of_last_review(card.id)?
.map(|ts| timing.next_day_at.elapsed_days_since(ts))
.map(|ts| timing.now.elapsed_secs_since(ts))
.unwrap_or_default() as u32;
let fsrs_retrievability = card
.memory_state
.zip(Some(days_elapsed))
.zip(Some(seconds_elapsed))
.zip(Some(card.decay.unwrap_or(FSRS5_DEFAULT_DECAY)))
.map(|((state, days), decay)| {
FSRS::new(None)
.unwrap()
.current_retrievability(state.into(), days, decay)
.map(|((state, seconds), decay)| {
FSRS::new(None).unwrap().current_retrievability_seconds(
state.into(),
seconds,
decay,
)
});
let original_deck = if card.original_deck_id == DeckId(0) {

View file

@ -85,7 +85,7 @@ impl CardData {
pub(crate) fn convert_to_json(&mut self) -> Result<String> {
if let Some(v) = &mut self.fsrs_stability {
round_to_places(v, 3)
round_to_places(v, 4)
}
if let Some(v) = &mut self.fsrs_difficulty {
round_to_places(v, 3)
@ -173,7 +173,7 @@ mod test {
};
assert_eq!(
data.convert_to_json().unwrap(),
r#"{"s":123.457,"d":1.235,"dr":0.99,"decay":0.123}"#
r#"{"s":123.4568,"d":1.235,"dr":0.99,"decay":0.123}"#
);
}
}

View file

@ -0,0 +1,49 @@
WITH searched_revlogs AS (
SELECT *
FROM revlog
WHERE ease > 0
AND cid IN search_cids
ORDER BY id DESC -- Use the last 10_000 reviews
LIMIT 10000
), average_pass AS (
SELECT AVG(time)
FROM searched_revlogs
WHERE ease > 1
),
lapse_count AS (
SELECT COUNT(time) AS lapse_count
FROM searched_revlogs
WHERE ease = 1
AND type = 1
),
fail_sum AS (
SELECT SUM(time) AS total_fail_time
FROM searched_revlogs
WHERE (
ease = 1
AND type = 1
)
OR type = 2
),
-- (sum(Relearning) + sum(Lapses)) / count(Lapses)
average_fail AS (
SELECT total_fail_time * 1.0 / NULLIF(lapse_count, 0) AS avg_fail_time
FROM fail_sum,
lapse_count
),
-- Can lead to cards with partial learn histories skewing the time
summed_learns AS (
SELECT cid,
SUM(time) AS total_time
FROM searched_revlogs
WHERE searched_revlogs.type = 0
GROUP BY cid
),
average_learn AS (
SELECT AVG(total_time) AS avg_learn_time
FROM summed_learns
)
SELECT *
FROM average_pass,
average_fail,
average_learn;

View file

@ -747,6 +747,20 @@ impl super::SqliteStorage {
.get(0)?)
}
pub(crate) fn get_costs_for_retention(&self) -> Result<(f32, f32, f32)> {
let mut statement = self
.db
.prepare(include_str!("get_costs_for_retention.sql"))?;
let mut query = statement.query(params![])?;
let row = query.next()?.unwrap();
Ok((
row.get(0).unwrap_or(7000.),
row.get(1).unwrap_or(23_000.),
row.get(2).unwrap_or(30_000.),
))
}
#[cfg(test)]
pub(crate) fn get_all_cards(&self) -> Vec<Card> {
self.db

View file

@ -158,7 +158,7 @@ impl SqliteStorage {
self.db
.prepare_cached(concat!(
include_str!("get.sql"),
" where ease between 1 and 4",
" where (ease between 1 and 4) or (ease = 0 and factor = 0)",
" order by cid, id"
))?
.query_and_then([], row_to_revlog_entry)?

View file

@ -315,7 +315,7 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
return Ok(None);
};
(next_day_at).saturating_sub(due) as u32 / 86_400
(next_day_at as u32).saturating_sub(due as u32) / 86_400
} else {
let Ok(ivl) = ctx.get_raw(2).as_i64() else {
return Ok(None);
@ -324,7 +324,7 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
return Ok(None);
};
let review_day = due.saturating_sub(ivl);
days_elapsed.saturating_sub(review_day) as u32
(days_elapsed as u32).saturating_sub(review_day as u32)
};
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
Ok(card_data.memory_state().map(|state| {
@ -359,14 +359,14 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
};
let days_elapsed = if due > 365_000 {
// (re)learning
next_day_at.saturating_sub(due) as u32 / 86_400
(next_day_at as u32).saturating_sub(due as u32) / 86_400
} else {
let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else {
return Ok(None);
};
let review_day = due.saturating_sub(interval);
days_elapsed.saturating_sub(review_day) as u32
(days_elapsed as u32).saturating_sub(review_day as u32)
};
if let Ok(card_data) = ctx.get_raw(0).as_str() {
if !card_data.is_empty() {

View file

@ -119,9 +119,13 @@ pub enum UploadResponse {
}
pub fn check_upload_limit(size: usize, limit: usize) -> Result<()> {
let size_of_one_mb: f64 = 1024.0 * 1024.0;
let collection_size_in_mb: f64 = size as f64 / size_of_one_mb;
let limit_size_in_mb: f64 = limit as f64 / size_of_one_mb;
if size >= limit {
Err(AnkiError::sync_error(
format!("{size} > {limit}"),
format!("{collection_size_in_mb:.2} MB > {limit_size_in_mb:.2} MB"),
SyncErrorKind::UploadTooLarge,
))
} else {

View file

@ -303,7 +303,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/>
<HandleLabel>
{#if isSizeConstrained}
{#if isSizeConstrained && !shrinkingDisabled}
<span>{`(${tr.editingDoubleClickToExpand()})`}</span>
{:else}
<span>{actualWidth}&times;{actualHeight}</span>

View file

@ -232,7 +232,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
placeHandle(true);
resetHandle();
}}
on:close={resetHandle}
on:close={() => {
placeHandle(true);
resetHandle();
}}
let:editor={mathjaxEditor}
>
<Shortcut

View file

@ -23,7 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let percentage = false;
let input: HTMLInputElement;
let focused = false;
export let focused = false;
let multiplier: number;
$: multiplier = percentage ? 100 : 1;
@ -129,6 +129,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
value={stringValue}
bind:this={input}
on:blur={update}
on:change={update}
on:input={onInput}
on:focusin={() => (focused = true)}
on:focusout={() => (focused = false)}

View file

@ -75,6 +75,9 @@ input[type="radio"],
input[type="checkbox"] {
cursor: pointer;
}
textarea,
input[type="date"],
input[type="text"] {
border-radius: prop(border-radius);
outline: none;

View file

@ -95,8 +95,8 @@
"repository": "https://github.com/TooTallNate/node-agent-base",
"publisher": "Nathan Rajlich",
"email": "nathan@tootallnate.net",
"path": "node_modules/agent-base",
"licenseFile": "node_modules/agent-base/README.md"
"path": "node_modules/http-proxy-agent/node_modules/agent-base",
"licenseFile": "node_modules/http-proxy-agent/node_modules/agent-base/README.md"
},
"asynckit@0.4.0": {
"licenses": "MIT",
@ -572,6 +572,14 @@
"path": "node_modules/lodash-es",
"licenseFile": "node_modules/lodash-es/LICENSE"
},
"lru-cache@10.4.3": {
"licenses": "ISC",
"repository": "https://github.com/isaacs/node-lru-cache",
"publisher": "Isaac Z. Schlueter",
"email": "i@izs.me",
"path": "node_modules/lru-cache",
"licenseFile": "node_modules/lru-cache/LICENSE"
},
"marked@5.1.2": {
"licenses": "MIT",
"repository": "https://github.com/markedjs/marked",
@ -768,16 +776,16 @@
"repository": "https://github.com/jsdom/whatwg-url",
"publisher": "Sebastian Mayr",
"email": "github@smayr.name",
"path": "node_modules/whatwg-url",
"licenseFile": "node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/jsdom/node_modules/whatwg-url",
"licenseFile": "node_modules/jsdom/node_modules/whatwg-url/LICENSE.txt"
},
"whatwg-url@11.0.0": {
"licenses": "MIT",
"repository": "https://github.com/jsdom/whatwg-url",
"publisher": "Sebastian Mayr",
"email": "github@smayr.name",
"path": "node_modules/data-urls/node_modules/whatwg-url",
"licenseFile": "node_modules/data-urls/node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/whatwg-url",
"licenseFile": "node_modules/whatwg-url/LICENSE.txt"
},
"ws@8.18.0": {
"licenses": "MIT",

View file

@ -15,6 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let stats: CardStatsResponse | null = null;
export let showRevlog: boolean = true;
export let showCurve: boolean = true;
$: fsrsEnabled = stats?.memoryState != null;
$: desiredRetention = stats?.desiredRetention ?? 0.9;
@ -41,7 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<Revlog revlog={stats.revlog} {fsrsEnabled} />
</Row>
{/if}
{#if fsrsEnabled}
{#if fsrsEnabled && showCurve}
<Row>
<ForgettingCurve revlog={stats.revlog} {desiredRetention} {decay} />
</Row>

View file

@ -12,6 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let data: PageData;
const showRevlog = $page.url.searchParams.get("revlog") !== "0";
const showCurve = $page.url.searchParams.get("curve") !== "0";
globalThis.anki ||= {};
globalThis.anki.updateCardInfos = async (card_id: string): Promise<void> => {
@ -25,11 +26,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<center>
{#if data.currentInfo}
<h3>Current</h3>
<CardInfo stats={data.currentInfo} {showRevlog} />
<CardInfo stats={data.currentInfo} {showRevlog} {showCurve} />
{/if}
{#if data.previousInfo}
<h3>Previous</h3>
<CardInfo stats={data.previousInfo} {showRevlog} />
<CardInfo stats={data.previousInfo} {showRevlog} {showCurve} />
{/if}
</center>

View file

@ -339,8 +339,11 @@ export function renderForgettingCurve(
1,
);
let text = tooltipText(d);
const desiredRetentionPercent = desiredRetention * 100;
if (y2 >= lineY - 10 && y2 <= lineY + 10) {
text += `<br>${tr.cardStatsFsrsForgettingCurveDesiredRetention()}: ${desiredRetention.toFixed(2)}`;
text += `<br>${tr.cardStatsFsrsForgettingCurveDesiredRetention()}: ${
desiredRetentionPercent.toFixed(0)
}%`;
}
showTooltip(text, x1, y1);
})

View file

@ -29,10 +29,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
width: 100%;
-webkit-appearance: none;
appearance: none;
background: var(--canvas-inset);
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 1px 0.5em;
outline: none !important;
}
</style>

View file

@ -11,6 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import {
computeFsrsParams,
evaluateParams,
getRetentionWorkload,
setWantsAbort,
} from "@generated/backend";
import * as tr from "@generated/ftl";
@ -26,11 +27,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ParamsInputRow from "./ParamsInputRow.svelte";
import ParamsSearchRow from "./ParamsSearchRow.svelte";
import SimulatorModal from "./SimulatorModal.svelte";
import { UpdateDeckConfigsMode } from "@generated/anki/deck_config_pb";
import {
GetRetentionWorkloadRequest,
UpdateDeckConfigsMode,
} from "@generated/anki/deck_config_pb";
export let state: DeckOptionsState;
export let openHelpModal: (String) => void;
export let onPresetChange: () => void;
export let newlyEnabled = false;
const config = state.currentConfig;
const defaults = state.defaults;
@ -39,6 +44,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: lastOptimizationWarning =
$daysSinceLastOptimization > 30 ? tr.deckConfigTimeToOptimize() : "";
let desiredRetentionFocused = false;
let desiredRetentionEverFocused = false;
let optimized = false;
const startingDesiredRetention = $config.desiredRetention.toFixed(2);
$: if (desiredRetentionFocused) {
desiredRetentionEverFocused = true;
}
$: showDesiredRetentionTooltip =
newlyEnabled || desiredRetentionEverFocused || optimized;
let computeParamsProgress: ComputeParamsProgress | undefined;
let computingParams = false;
@ -47,10 +61,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: computing = computingParams || checkingParams;
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
$: desiredRetentionWarning = getRetentionWarning(
roundedRetention,
fsrsParams($config),
);
$: desiredRetentionWarning = getRetentionLongShortWarning(roundedRetention);
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
const WORKLOAD_UPDATE_DELAY_MS = 100;
let desiredRetentionChangeInfo = "";
$: {
clearTimeout(timeoutId);
if (showDesiredRetentionTooltip) {
timeoutId = setTimeout(() => {
getRetentionChangeInfo(roundedRetention, fsrsParams($config));
}, WORKLOAD_UPDATE_DELAY_MS);
} else {
desiredRetentionChangeInfo = "";
}
}
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
@ -67,23 +94,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
reviewOrder: $config.reviewOrder,
});
function getRetentionWarning(retention: number, params: number[]): string {
const decay = params.length > 20 ? -params[20] : -0.5; // default decay for FSRS-4.5 and FSRS-5
const factor = 0.9 ** (1 / decay) - 1;
const stability = 100;
const days = Math.round(
(stability / factor) * (Math.pow(retention, 1 / decay) - 1),
);
if (days === 100) {
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
const DESIRED_RETENTION_HIGH_THRESHOLD = 0.95;
function getRetentionLongShortWarning(retention: number) {
if (retention < DESIRED_RETENTION_LOW_THRESHOLD) {
return tr.deckConfigDesiredRetentionTooLow();
} else if (retention > DESIRED_RETENTION_HIGH_THRESHOLD) {
return tr.deckConfigDesiredRetentionTooHigh();
} else {
return "";
}
return tr.deckConfigA100DayInterval({ days });
}
async function getRetentionChangeInfo(retention: number, params: number[]) {
if (+startingDesiredRetention == roundedRetention) {
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorUnchanged();
return;
}
const request = new GetRetentionWorkloadRequest({
w: params,
search: defaultparamSearch,
before: +startingDesiredRetention,
after: retention,
});
const resp = await getRetentionWorkload(request);
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorChange({
factor: resp.factor.toFixed(2),
previousDr: (+startingDesiredRetention * 100).toString(),
});
}
function getRetentionWarningClass(retention: number): string {
if (retention < 0.7 || retention > 0.97) {
return "alert-danger";
} else if (retention < 0.8 || retention > 0.95) {
} else if (
retention < DESIRED_RETENTION_LOW_THRESHOLD ||
retention > DESIRED_RETENTION_HIGH_THRESHOLD
) {
return "alert-warning";
} else {
return "alert-info";
@ -146,6 +194,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setTimeout(() => alert(msg), 200);
} else {
$config.fsrsParams6 = resp.params;
optimized = true;
}
if (computeParamsProgress) {
computeParamsProgress.current = computeParamsProgress.total;
@ -237,12 +286,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
min={0.7}
max={0.99}
percentage={true}
bind:focused={desiredRetentionFocused}
>
<SettingTitle on:click={() => openHelpModal("desiredRetention")}>
{tr.deckConfigDesiredRetention()}
</SettingTitle>
</SpinBoxFloatRow>
<Warning warning={desiredRetentionChangeInfo} className={"alert-info two-line"} />
<Warning warning={desiredRetentionWarning} className={retentionWarningClass} />
<div class="ms-1 me-1">
@ -331,4 +382,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.btn {
margin-bottom: 0.375rem;
}
:global(.two-line) {
white-space: pre-wrap;
min-height: calc(2ch + 30px);
box-sizing: content-box;
display: flex;
align-content: center;
flex-wrap: wrap;
}
</style>

View file

@ -25,6 +25,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let onPresetChange: () => void;
const fsrs = state.fsrs;
let newlyEnabled = false;
$: if (!$fsrs) {
newlyEnabled = true;
}
const settings = {
fsrs: {
@ -94,6 +98,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#if $fsrs}
<FsrsOptions
{state}
{newlyEnabled}
openHelpModal={(key) =>
openHelpModal(Object.keys(settings).indexOf(key))}
{onPresetChange}

View file

@ -3,11 +3,26 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { tick } from "svelte";
export let value: number[];
export let defaults: number[];
let stringValue: string;
$: stringValue = render(value);
let taRef: HTMLTextAreaElement;
function updateHeight() {
if (taRef) {
taRef.style.height = "auto";
// +2 for "overflow-y: auto" in case js breaks
taRef.style.height = `${taRef.scrollHeight + 2}px`;
}
}
$: {
stringValue = render(value);
tick().then(updateHeight);
}
function render(params: number[]): string {
return params.map((v) => v.toFixed(4)).join(", ");
@ -22,9 +37,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
</script>
<svelte:window onresize={updateHeight} />
<textarea
bind:this={taRef}
value={stringValue}
on:blur={update}
class="w-100"
placeholder={render(defaults)}
></textarea>
<style>
textarea {
resize: none;
overflow-y: auto;
}
</style>

View file

@ -15,6 +15,7 @@
export let max = 9999;
export let step = 0.01;
export let percentage = false;
export let focused = false;
</script>
<Row --cols={13}>
@ -23,7 +24,7 @@
</Col>
<Col --col-size={6} breakpoint="xs">
<ConfigInput>
<SpinBox bind:value {min} {max} {step} {percentage} />
<SpinBox bind:value {min} {max} {step} {percentage} bind:focused />
<RevertButton slot="revert" bind:value {defaultValue} />
</ConfigInput>
</Col>

View file

@ -127,11 +127,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
.search-link {
border: none;
border: 1px transparent solid;
background: transparent;
cursor: pointer;
box-shadow: none;
padding: 1px 3px;
padding: 0 2px;
margin-bottom: 0px;
}

View file

@ -112,6 +112,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div class="range-box-pad"></div>
<style lang="scss">
label {
display: inline-flex;
align-items: center;
}
input[type="radio"] {
margin-inline-end: 0.3em;
}
.range-box {
position: sticky;
z-index: 1;

View file

@ -148,7 +148,7 @@ export function renderButtons(
kind = tr.statisticsCountsMatureCards();
break;
}
return `${kind} \u200e(${totalCorrect(d).percent}%)`;
return `${kind}`;
}) as any,
)
.tickSizeOuter(0),
@ -222,8 +222,23 @@ export function renderButtons(
const button = tr.statisticsAnswerButtonsButtonNumber();
const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
const correctStrInfo = tr.statisticsHoursCorrectInfo();
const pressedStr = `${timesPressed}: ${totalPressedStr(d)}`;
return `${button}: ${d.buttonNum}<br>${pressedStr}<br>${correctStr}`;
let buttonText: string;
if (d.buttonNum === 1) {
buttonText = tr.studyingAgain();
} else if (d.buttonNum === 2) {
buttonText = tr.studyingHard();
} else if (d.buttonNum === 3) {
buttonText = tr.studyingGood();
} else if (d.buttonNum === 4) {
buttonText = tr.studyingEasy();
} else {
buttonText = "";
}
return `${button}: ${d.buttonNum} (${buttonText})<br>${pressedStr}<br>${correctStr} ${correctStrInfo}`;
}
svg.select("g.hover-columns")

View file

@ -299,7 +299,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? 'left-border-radius'
: 'right-border-radius'}"
{iconSize}
on:click={tool.action}
on:click={() => {
tool.action();
handleToolChanges(activeTool);
}}
tooltip="{tool.tooltip()} ({getPlatformString(tool.shortcut)})"
disabled={tool.name === "undo"
? !$undoStack.undoable

View file

@ -95,12 +95,10 @@ function initCanvas(): fabric.Canvas {
undoStack.setCanvas(canvas);
// find object per-pixel basis rather than according to bounding box,
// allow click through transparent area
canvas.perPixelTargetFind = true;
fabric.Object.prototype.perPixelTargetFind = true;
// Disable uniform scaling
canvas.uniformScaling = false;
canvas.uniScaleKey = "none";
// disable rotation globally
delete fabric.Object.prototype.controls.mtr;
// disable object caching
fabric.Object.prototype.objectCaching = false;
// add a border to corner to handle blend of control
@ -108,12 +106,16 @@ function initCanvas(): fabric.Canvas {
fabric.Object.prototype.cornerStyle = "circle";
fabric.Object.prototype.cornerStrokeColor = "#000000";
fabric.Object.prototype.padding = 8;
// disable rotation when selecting
canvas.on("selection:created", () => {
const g = canvas.getActiveObject();
if (g && g instanceof fabric.Group) { g.setControlsVisibility({ mtr: false }); }
});
canvas.on("object:modified", (evt) => {
if (evt.target instanceof fabric.Polygon) {
modifiedPolygon(canvas, evt.target);
undoStack.onObjectModified();
}
saveNeededStore.set(true);
});
canvas.on("text:editing:entered", function() {
textEditingState.set(true);

View file

@ -263,15 +263,29 @@ function drawShape({
ctx.fillStyle = fill;
ctx.strokeStyle = stroke;
ctx.lineWidth = strokeWidth;
const angle = ((shape.angle ?? 0) * Math.PI) / 180;
if (shape instanceof Rectangle) {
if (angle) {
ctx.save();
ctx.translate(shape.left, shape.top);
ctx.rotate(angle);
ctx.translate(-shape.left, -shape.top);
}
ctx.fillRect(shape.left, shape.top, shape.width, shape.height);
// ctx stroke methods will draw a visible stroke, even if the width is 0
if (strokeWidth) {
ctx.strokeRect(shape.left, shape.top, shape.width, shape.height);
}
if (angle) { ctx.restore(); }
} else if (shape instanceof Ellipse) {
const adjustedLeft = shape.left + shape.rx;
const adjustedTop = shape.top + shape.ry;
if (angle) {
ctx.save();
ctx.translate(shape.left, shape.top);
ctx.rotate(angle);
ctx.translate(-shape.left, -shape.top);
}
ctx.beginPath();
ctx.ellipse(
adjustedLeft,
@ -288,6 +302,7 @@ function drawShape({
if (strokeWidth) {
ctx.stroke();
}
if (angle) { ctx.restore(); }
} else if (shape instanceof Polygon) {
const offset = getPolygonOffset(shape);
ctx.save();
@ -329,10 +344,17 @@ function drawShape({
}
totalHeight += lineHeight;
}
const left = shape.left / shape.scaleX;
const top = shape.top / shape.scaleY;
if (angle) {
ctx.translate(left, top);
ctx.rotate(angle);
ctx.translate(-left, -top);
}
ctx.fillStyle = TEXT_BACKGROUND_COLOR;
ctx.fillRect(
shape.left / shape.scaleX,
shape.top / shape.scaleY,
left,
top,
maxWidth + TEXT_PADDING,
totalHeight + TEXT_PADDING,
);

View file

@ -5,7 +5,7 @@ import { fabric } from "fabric";
import { SHAPE_MASK_COLOR } from "../tools/lib";
import type { ConstructorParams, Size } from "../types";
import { floatToDisplay } from "./floats";
import { angleToStored, floatToDisplay } from "./lib";
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
export type ShapeOrShapes = Shape | Shape[];
@ -18,6 +18,7 @@ export type ShapeOrShapes = Shape | Shape[];
export class Shape {
left: number;
top: number;
angle?: number; // polygons don't use it
fill: string;
/** Whether occlusions from other cloze numbers should be shown on the
* question side. Used only in reviewer code.
@ -25,13 +26,15 @@ export class Shape {
occludeInactive?: boolean;
/* Cloze ordinal */
ordinal: number | undefined;
id: string | undefined;
constructor(
{ left = 0, top = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }: ConstructorParams<Shape> =
{},
{ left = 0, top = 0, angle = 0, fill = SHAPE_MASK_COLOR, occludeInactive, ordinal = undefined }:
ConstructorParams<Shape> = {},
) {
this.left = left;
this.top = top;
this.angle = angle;
this.fill = fill;
this.occludeInactive = occludeInactive;
this.ordinal = ordinal;
@ -41,9 +44,11 @@ export class Shape {
* text.
*/
toDataForCloze(): ShapeDataForCloze {
const angle = angleToStored(this.angle);
return {
left: floatToDisplay(this.left),
top: floatToDisplay(this.top),
...(!angle ? {} : { angle: angle.toString() }),
...(this.fill === SHAPE_MASK_COLOR ? {} : { fill: this.fill }),
};
}
@ -85,6 +90,7 @@ export class Shape {
export interface ShapeDataForCloze {
left: string;
top: string;
angle?: string;
fill?: string;
oi?: string;
}

View file

@ -6,7 +6,7 @@ import { fabric } from "fabric";
import type { ConstructorParams, Size } from "../types";
import type { ShapeDataForCloze } from "./base";
import { Shape } from "./base";
import { floatToDisplay } from "./floats";
import { floatToDisplay } from "./lib";
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
export class Ellipse extends Shape {
@ -17,6 +17,7 @@ export class Ellipse extends Shape {
super(rest);
this.rx = rx;
this.ry = ry;
this.id = "ellipse-" + new Date().getTime();
}
toDataForCloze(): EllipseDataForCloze {

View file

@ -9,6 +9,7 @@ import type { GetImageOcclusionNoteResponse_ImageOcclusion } from "@generated/an
import type { Shape, ShapeOrShapes } from "./base";
import { Ellipse } from "./ellipse";
import { storedToAngle } from "./lib";
import { Point, Polygon } from "./polygon";
import { Rectangle } from "./rectangle";
import { Text } from "./text";
@ -75,6 +76,7 @@ function extractShapeFromRenderedCloze(cloze: HTMLDivElement): Shape | null {
text: cloze.dataset.text,
scale: cloze.dataset.scale,
fs: cloze.dataset.fontSize,
angle: cloze.dataset.angle,
};
return buildShape(type, props);
}
@ -92,6 +94,7 @@ function buildShape(type: ShapeType, props: Record<string, any>): Shape {
props.top = parseFloat(
Number.isNaN(Number(props.top)) ? ".0000" : props.top,
);
props.angle = storedToAngle(props.angle) ?? 0;
switch (type) {
case "rect": {
return new Rectangle({

View file

@ -11,3 +11,15 @@ export function floatToDisplay(number: number): string {
}
return number.toFixed(4).replace(/^0+|0+$/g, "");
}
const ANGLE_STEPS = 10000;
export function angleToStored(angle: any): number | null {
const angleDeg = Number(angle) % 360;
return Number.isNaN(angleDeg) ? null : Math.round((angleDeg / 360) * ANGLE_STEPS);
}
export function storedToAngle(x: any): number | null {
const angleSteps = Number(x) % ANGLE_STEPS;
return Number.isNaN(angleSteps) ? null : (angleSteps / ANGLE_STEPS) * 360;
}

View file

@ -6,7 +6,7 @@ import { fabric } from "fabric";
import type { ConstructorParams, Size } from "../types";
import type { ShapeDataForCloze } from "./base";
import { Shape } from "./base";
import { floatToDisplay } from "./floats";
import { floatToDisplay } from "./lib";
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
export class Polygon extends Shape {
@ -15,6 +15,7 @@ export class Polygon extends Shape {
constructor({ points = [], ...rest }: ConstructorParams<Polygon> = {}) {
super(rest);
this.points = points;
this.id = "polygon-" + new Date().getTime();
}
toDataForCloze(): PolygonDataForCloze {

View file

@ -6,7 +6,7 @@ import { fabric } from "fabric";
import type { ConstructorParams, Size } from "../types";
import type { ShapeDataForCloze } from "./base";
import { Shape } from "./base";
import { floatToDisplay } from "./floats";
import { floatToDisplay } from "./lib";
import { xFromNormalized, xToNormalized, yFromNormalized, yToNormalized } from "./position";
export class Rectangle extends Shape {
@ -17,6 +17,7 @@ export class Rectangle extends Shape {
super(rest);
this.width = width;
this.height = height;
this.id = "rect-" + new Date().getTime();
}
toDataForCloze(): RectangleDataForCloze {

View file

@ -7,7 +7,7 @@ import { TEXT_BACKGROUND_COLOR, TEXT_COLOR, TEXT_FONT_FAMILY, TEXT_FONT_SIZE, TE
import type { ConstructorParams, Size } from "../types";
import type { ShapeDataForCloze } from "./base";
import { Shape } from "./base";
import { floatToDisplay } from "./floats";
import { floatToDisplay } from "./lib";
export class Text extends Shape {
text: string;
@ -28,6 +28,7 @@ export class Text extends Shape {
this.scaleX = scaleX;
this.scaleY = scaleY;
this.fontSize = fontSize;
this.id = "text-" + new Date().getTime();
}
toDataForCloze(): TextDataForCloze {

View file

@ -4,7 +4,7 @@
import { fabric } from "fabric";
import { get } from "svelte/store";
import { opacityStateStore } from "../store";
import { opacityStateStore, saveNeededStore } from "../store";
import type { Size } from "../types";
export const SHAPE_MASK_COLOR = "#ffeba2";
@ -76,7 +76,7 @@ export const groupShapes = (canvas: fabric.Canvas): void => {
activeObject.toGroup().set({
opacity: get(opacityStateStore) ? 0.4 : 1,
});
}).setControlsVisibility({ mtr: false });
redraw(canvas);
};
@ -228,19 +228,23 @@ const setShapePosition = (
boundingBox: fabric.Rect,
object: fabric.Object,
): void => {
if (object.left! < 0) {
object.set({ left: 0 });
const { left, top, width, height } = object.getBoundingRect(true);
if (left < 0) {
object.set({ left: Math.max(object.left! - left, 0) });
}
if (object.top! < 0) {
object.set({ top: 0 });
if (top < 0) {
object.set({ top: Math.max(object.top! - top, 0) });
}
if (object.left! + object.width! * object.scaleX! + object.strokeWidth! > boundingBox.width!) {
object.set({ left: boundingBox.width! - object.width! * object.scaleX! });
if (left > boundingBox.width!) {
object.set({ left: object.left! - left - width + boundingBox.width! });
}
if (object.top! + object.height! * object.scaleY! + object.strokeWidth! > boundingBox.height!) {
object.set({ top: boundingBox.height! - object.height! * object.scaleY! });
if (top > boundingBox.height!) {
object.set({ top: object.top! - top - height + boundingBox.height! });
}
object.setCoords();
saveNeededStore.set(true);
};
export function enableUniformScaling(canvas: fabric.Canvas, obj: fabric.Object): void {
@ -260,6 +264,8 @@ export function enableUniformScaling(canvas: fabric.Canvas, obj: fabric.Object):
export function addBorder(obj: fabric.Object): void {
obj.stroke = BORDER_COLOR;
obj.strokeWidth = 1;
obj.strokeUniform = true;
}
export const redraw = (canvas: fabric.Canvas): void => {
@ -277,23 +283,25 @@ export const makeShapesRemainInCanvas = (canvas: fabric.Canvas, boundingBox: fab
canvas.on("object:moving", function(e) {
const obj = e.target!;
const objWidth = obj.getScaledWidth();
const objHeight = obj.getScaledHeight();
const { left: objBbLeft, top: objBbTop, width: objBbWidth, height: objBbHeight } = obj.getBoundingRect(
true,
true,
);
if (objWidth > boundingBox.width! || objHeight > boundingBox.height!) {
if (objBbWidth > boundingBox.width! || objBbHeight > boundingBox.height!) {
return;
}
const top = obj.top!;
const left = obj.left!;
const topBound = boundingBox.top!;
const bottomBound = topBound + boundingBox.height! + 5;
const leftBound = boundingBox.left!;
const rightBound = leftBound + boundingBox.width! + 5;
obj.left = Math.min(Math.max(left, leftBound), rightBound - objWidth);
obj.top = Math.min(Math.max(top, topBound), bottomBound - objHeight);
const newBbLeft = Math.min(Math.max(objBbLeft, leftBound), rightBound - objBbWidth);
const newBbTop = Math.min(Math.max(objBbTop, topBound), bottomBound - objBbHeight);
obj.left = obj.left! + newBbLeft - objBbLeft;
obj.top = obj.top! + newBbTop - objBbTop;
});
};

View file

@ -87,22 +87,23 @@ const addPoint = (canvas: fabric.Canvas, options): void => {
const point = new fabric.Circle({
radius: 5,
fill: "#ffffff",
fill: "transparent",
stroke: "#333333",
strokeWidth: 0.5,
originX: "left",
originY: "top",
strokeWidth: 1.5,
originX: "center",
originY: "center",
left: origX,
top: origY,
selectable: false,
hasBorders: false,
hasControls: false,
objectCaching: false,
perPixelTargetFind: false,
});
if (pointsList.length === 0) {
point.set({
fill: "red",
stroke: "red",
});
}
@ -112,8 +113,8 @@ const addPoint = (canvas: fabric.Canvas, options): void => {
strokeWidth: 2,
fill: "#999999",
stroke: "#999999",
originX: "left",
originY: "top",
originX: "center",
originY: "center",
selectable: false,
hasBorders: false,
hasControls: false,
@ -196,6 +197,7 @@ const generatePolygon = (canvas: fabric.Canvas, pointsList): void => {
strokeWidth: 1,
strokeUniform: true,
noScaleCache: false,
selectable: false,
opacity: get(opacityStateStore) ? 0.4 : 1,
});
polygon["id"] = "polygon-" + new Date().getTime();

View file

@ -116,8 +116,8 @@ class UndoStack {
}
private push(): void {
const entry = JSON.stringify(this.canvas);
if (entry === this.stack[this.stack.length - 1]) {
const entry = JSON.stringify(this.canvas?.toJSON(["id"]));
if (entry === this.stack[this.index]) {
return;
}
this.stack.length = this.index + 1;

View file

@ -65,7 +65,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<SettingTitle
on:click={() => openHelpModal(Object.keys(settings).indexOf("delimiter"))}
>
{settings.delimiter.title}
{$metadata.forceDelimiter
? settings.delimiter.title
: tr.importingFieldSeparatorGuessed()}
</SettingTitle>
</EnumSelectorRow>

257
yarn.lock
View file

@ -113,9 +113,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/aix-ppc64@npm:0.25.0"
"@esbuild/aix-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/aix-ppc64@npm:0.25.3"
conditions: os=aix & cpu=ppc64
languageName: node
linkType: hard
@ -134,9 +134,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/android-arm64@npm:0.25.0"
"@esbuild/android-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm64@npm:0.25.3"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
@ -155,9 +155,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-arm@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/android-arm@npm:0.25.0"
"@esbuild/android-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm@npm:0.25.3"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
@ -176,9 +176,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/android-x64@npm:0.25.0"
"@esbuild/android-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-x64@npm:0.25.3"
conditions: os=android & cpu=x64
languageName: node
linkType: hard
@ -197,9 +197,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/darwin-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/darwin-arm64@npm:0.25.0"
"@esbuild/darwin-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-arm64@npm:0.25.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
@ -218,9 +218,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/darwin-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/darwin-x64@npm:0.25.0"
"@esbuild/darwin-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-x64@npm:0.25.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
@ -239,9 +239,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/freebsd-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/freebsd-arm64@npm:0.25.0"
"@esbuild/freebsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-arm64@npm:0.25.3"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
@ -260,9 +260,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/freebsd-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/freebsd-x64@npm:0.25.0"
"@esbuild/freebsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-x64@npm:0.25.3"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
@ -281,9 +281,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-arm64@npm:0.25.0"
"@esbuild/linux-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm64@npm:0.25.3"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
@ -302,9 +302,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-arm@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-arm@npm:0.25.0"
"@esbuild/linux-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm@npm:0.25.3"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
@ -323,9 +323,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-ia32@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-ia32@npm:0.25.0"
"@esbuild/linux-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ia32@npm:0.25.3"
conditions: os=linux & cpu=ia32
languageName: node
linkType: hard
@ -344,9 +344,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-loong64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-loong64@npm:0.25.0"
"@esbuild/linux-loong64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-loong64@npm:0.25.3"
conditions: os=linux & cpu=loong64
languageName: node
linkType: hard
@ -365,9 +365,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-mips64el@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-mips64el@npm:0.25.0"
"@esbuild/linux-mips64el@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-mips64el@npm:0.25.3"
conditions: os=linux & cpu=mips64el
languageName: node
linkType: hard
@ -386,9 +386,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-ppc64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-ppc64@npm:0.25.0"
"@esbuild/linux-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ppc64@npm:0.25.3"
conditions: os=linux & cpu=ppc64
languageName: node
linkType: hard
@ -407,9 +407,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-riscv64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-riscv64@npm:0.25.0"
"@esbuild/linux-riscv64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-riscv64@npm:0.25.3"
conditions: os=linux & cpu=riscv64
languageName: node
linkType: hard
@ -428,9 +428,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-s390x@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-s390x@npm:0.25.0"
"@esbuild/linux-s390x@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-s390x@npm:0.25.3"
conditions: os=linux & cpu=s390x
languageName: node
linkType: hard
@ -449,16 +449,16 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/linux-x64@npm:0.25.0"
"@esbuild/linux-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-x64@npm:0.25.3"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@esbuild/netbsd-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/netbsd-arm64@npm:0.25.0"
"@esbuild/netbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-arm64@npm:0.25.3"
conditions: os=netbsd & cpu=arm64
languageName: node
linkType: hard
@ -477,16 +477,16 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/netbsd-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/netbsd-x64@npm:0.25.0"
"@esbuild/netbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-x64@npm:0.25.3"
conditions: os=netbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/openbsd-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/openbsd-arm64@npm:0.25.0"
"@esbuild/openbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-arm64@npm:0.25.3"
conditions: os=openbsd & cpu=arm64
languageName: node
linkType: hard
@ -505,9 +505,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/openbsd-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/openbsd-x64@npm:0.25.0"
"@esbuild/openbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-x64@npm:0.25.3"
conditions: os=openbsd & cpu=x64
languageName: node
linkType: hard
@ -526,9 +526,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/sunos-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/sunos-x64@npm:0.25.0"
"@esbuild/sunos-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/sunos-x64@npm:0.25.3"
conditions: os=sunos & cpu=x64
languageName: node
linkType: hard
@ -547,9 +547,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-arm64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/win32-arm64@npm:0.25.0"
"@esbuild/win32-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-arm64@npm:0.25.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
@ -568,9 +568,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-ia32@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/win32-ia32@npm:0.25.0"
"@esbuild/win32-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-ia32@npm:0.25.3"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
@ -589,9 +589,9 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-x64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/win32-x64@npm:0.25.0"
"@esbuild/win32-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-x64@npm:0.25.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -2018,8 +2018,8 @@ __metadata:
d3: "npm:^7.0.0"
diff: "npm:^5.0.0"
dprint: "npm:^0.47.2"
esbuild: "npm:^0.25.0"
esbuild-sass-plugin: "npm:^2"
esbuild: "npm:^0.25.3"
esbuild-sass-plugin: "npm:^3.3.1"
esbuild-svelte: "npm:^0.9.2"
eslint: "npm:^8.44.0"
eslint-plugin-compat: "npm:^4.1.4"
@ -2046,7 +2046,7 @@ __metadata:
tslib: "npm:^2.0.3"
tsx: "npm:^3.12.0"
typescript: "npm:^5.0.4"
vite: "npm:5.4.18"
vite: "npm:5.4.19"
vitest: "npm:^2"
languageName: unknown
linkType: soft
@ -3396,15 +3396,17 @@ __metadata:
languageName: node
linkType: hard
"esbuild-sass-plugin@npm:^2":
version: 2.16.1
resolution: "esbuild-sass-plugin@npm:2.16.1"
"esbuild-sass-plugin@npm:^3.3.1":
version: 3.3.1
resolution: "esbuild-sass-plugin@npm:3.3.1"
dependencies:
resolve: "npm:^1.22.6"
sass: "npm:^1.7.3"
resolve: "npm:^1.22.8"
safe-identifier: "npm:^0.4.2"
sass: "npm:^1.71.1"
peerDependencies:
esbuild: ^0.19.4
checksum: 10c0/2e0eedfb5863642cd03712a780f6a1896efbb17d41f7de7ae0558948077bb1a9b55b1bc1e235562939c14b528ddacfd8a3eb255eaceffabfa02489fd64403af6
esbuild: ">=0.20.1"
sass-embedded: ^1.71.1
checksum: 10c0/bbbef049ebe58c449caa8db0c893abbaf97c4b83ca349d8c541c11503092475afc4ed504218987cb745d61bfb1a26a19118688a450057a43e3943d2224ee2060
languageName: node
linkType: hard
@ -3500,35 +3502,35 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:^0.25.0":
version: 0.25.0
resolution: "esbuild@npm:0.25.0"
"esbuild@npm:^0.25.3":
version: 0.25.3
resolution: "esbuild@npm:0.25.3"
dependencies:
"@esbuild/aix-ppc64": "npm:0.25.0"
"@esbuild/android-arm": "npm:0.25.0"
"@esbuild/android-arm64": "npm:0.25.0"
"@esbuild/android-x64": "npm:0.25.0"
"@esbuild/darwin-arm64": "npm:0.25.0"
"@esbuild/darwin-x64": "npm:0.25.0"
"@esbuild/freebsd-arm64": "npm:0.25.0"
"@esbuild/freebsd-x64": "npm:0.25.0"
"@esbuild/linux-arm": "npm:0.25.0"
"@esbuild/linux-arm64": "npm:0.25.0"
"@esbuild/linux-ia32": "npm:0.25.0"
"@esbuild/linux-loong64": "npm:0.25.0"
"@esbuild/linux-mips64el": "npm:0.25.0"
"@esbuild/linux-ppc64": "npm:0.25.0"
"@esbuild/linux-riscv64": "npm:0.25.0"
"@esbuild/linux-s390x": "npm:0.25.0"
"@esbuild/linux-x64": "npm:0.25.0"
"@esbuild/netbsd-arm64": "npm:0.25.0"
"@esbuild/netbsd-x64": "npm:0.25.0"
"@esbuild/openbsd-arm64": "npm:0.25.0"
"@esbuild/openbsd-x64": "npm:0.25.0"
"@esbuild/sunos-x64": "npm:0.25.0"
"@esbuild/win32-arm64": "npm:0.25.0"
"@esbuild/win32-ia32": "npm:0.25.0"
"@esbuild/win32-x64": "npm:0.25.0"
"@esbuild/aix-ppc64": "npm:0.25.3"
"@esbuild/android-arm": "npm:0.25.3"
"@esbuild/android-arm64": "npm:0.25.3"
"@esbuild/android-x64": "npm:0.25.3"
"@esbuild/darwin-arm64": "npm:0.25.3"
"@esbuild/darwin-x64": "npm:0.25.3"
"@esbuild/freebsd-arm64": "npm:0.25.3"
"@esbuild/freebsd-x64": "npm:0.25.3"
"@esbuild/linux-arm": "npm:0.25.3"
"@esbuild/linux-arm64": "npm:0.25.3"
"@esbuild/linux-ia32": "npm:0.25.3"
"@esbuild/linux-loong64": "npm:0.25.3"
"@esbuild/linux-mips64el": "npm:0.25.3"
"@esbuild/linux-ppc64": "npm:0.25.3"
"@esbuild/linux-riscv64": "npm:0.25.3"
"@esbuild/linux-s390x": "npm:0.25.3"
"@esbuild/linux-x64": "npm:0.25.3"
"@esbuild/netbsd-arm64": "npm:0.25.3"
"@esbuild/netbsd-x64": "npm:0.25.3"
"@esbuild/openbsd-arm64": "npm:0.25.3"
"@esbuild/openbsd-x64": "npm:0.25.3"
"@esbuild/sunos-x64": "npm:0.25.3"
"@esbuild/win32-arm64": "npm:0.25.3"
"@esbuild/win32-ia32": "npm:0.25.3"
"@esbuild/win32-x64": "npm:0.25.3"
dependenciesMeta:
"@esbuild/aix-ppc64":
optional: true
@ -3582,7 +3584,7 @@ __metadata:
optional: true
bin:
esbuild: bin/esbuild
checksum: 10c0/5767b72da46da3cfec51661647ec850ddbf8a8d0662771139f10ef0692a8831396a0004b2be7966cecdb08264fb16bdc16290dcecd92396fac5f12d722fa013d
checksum: 10c0/127aff654310ede4e2eb232a7b1d8823f5b5d69222caf17aa7f172574a5b6b75f71ce78c6d8a40030421d7c75b784dc640de0fb1b87b7ea77ab2a1c832fa8df8
languageName: node
linkType: hard
@ -5860,7 +5862,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.22.6":
"resolve@npm:^1.22.8":
version: 1.22.10
resolution: "resolve@npm:1.22.10"
dependencies:
@ -5886,7 +5888,7 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@npm%3A^1.22.6#optional!builtin<compat/resolve>":
"resolve@patch:resolve@npm%3A^1.22.8#optional!builtin<compat/resolve>":
version: 1.22.10
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d"
dependencies:
@ -6062,6 +6064,13 @@ __metadata:
languageName: node
linkType: hard
"safe-identifier@npm:^0.4.2":
version: 0.4.2
resolution: "safe-identifier@npm:0.4.2"
checksum: 10c0/a6b0cdb5347e48c5ea4ddf4cdca5359b12529a11a7368225c39f882fcc0e679c81e82e3b13e36bd27ba7bdec9286f4cc062e3e527464d93ba61290b6e0bc6747
languageName: node
linkType: hard
"safe-regex-test@npm:^1.0.3":
version: 1.0.3
resolution: "safe-regex-test@npm:1.0.3"
@ -6105,9 +6114,9 @@ __metadata:
languageName: node
linkType: hard
"sass@npm:^1.7.3":
version: 1.85.0
resolution: "sass@npm:1.85.0"
"sass@npm:^1.71.1":
version: 1.87.0
resolution: "sass@npm:1.87.0"
dependencies:
"@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0"
@ -6118,7 +6127,7 @@ __metadata:
optional: true
bin:
sass: sass.js
checksum: 10c0/a1af0c0596ae1904f66337d0c70a684db6e12210f97be4326cc3dcf18b0f956d7bc45ab2bcc7a8422d433d3eb3c9cb2cc8e60b2dafbdd01fb1ae5a23f5424690
checksum: 10c0/bd245faf14e4783dc547765350cf05817edaac0d6d6f6e4da8ab751f3eb3cc3873afd563c0ce416a24aa6c9c4e9023b05096447fc006660a01f76adffb54fbc6
languageName: node
linkType: hard
@ -7030,9 +7039,9 @@ __metadata:
languageName: node
linkType: hard
"vite@npm:5.4.18":
version: 5.4.18
resolution: "vite@npm:5.4.18"
"vite@npm:5.4.19":
version: 5.4.19
resolution: "vite@npm:5.4.19"
dependencies:
esbuild: "npm:^0.21.3"
fsevents: "npm:~2.3.3"
@ -7069,7 +7078,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10c0/a8cbbec6bdf399e62c386d70b8485e4f2f1b427beb19bc7c5d52b402a0c3750b7ff469fc20a8333755ea13bc1b0af5df3f22c8fd37d1739ee51d709b7a4740b6
checksum: 10c0/c97601234dba482cea5290f2a2ea0fcd65e1fab3df06718ea48adc8ceb14bc3129508216c4989329c618f6a0470b42f439677a207aef62b0c76f445091c2d89e
languageName: node
linkType: hard