mirror of
https://github.com/ankitects/anki.git
synced 2026-01-13 22:13:58 -05:00
Merge branch 'main' into Feat/evaluate-FSRS-with-time-series-split
This commit is contained in:
commit
60f4017bc5
85 changed files with 830 additions and 314 deletions
2
.cursor/rules/building.md
Normal file
2
.cursor/rules/building.md
Normal 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
7
.cursor/rules/i18n.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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**:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'<br>', it will appear as a line break on your card. On the other hand, with this
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -221,9 +221,12 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
|||
"Yuki",
|
||||
"🦙 (siid)",
|
||||
"Mukunda Madhav Dey",
|
||||
"Adnane Taghi",
|
||||
"Anon_0000",
|
||||
)
|
||||
)
|
||||
|
||||
allusers = [user.replace(" ", " ") for user in allusers]
|
||||
abouttext += "<p>" + tr.about_written_by_damien_elmes_with_patches(
|
||||
cont=", ".join(allusers) + f", {tr.about_and_others()}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ exposed_backend_list = [
|
|||
"simulate_fsrs_review",
|
||||
# DeckConfigService
|
||||
"get_ignored_before_count",
|
||||
"get_retention_workload",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
71
qt/aqt/url_schemes.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -368,8 +368,6 @@ impl Collection {
|
|||
}))
|
||||
),
|
||||
)?;
|
||||
} else if card.queue == CardQueue::Suspended {
|
||||
invalid_input!("Can't answer suspended cards");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -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 = &[
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
rslib/src/storage/card/get_costs_for_retention.sql
Normal file
49
rslib/src/storage/card/get_costs_for_retention.sql
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)?
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}×{actualHeight}</span>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
257
yarn.lock
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue