mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 16:26:40 -04:00
Merge branch 'main' into fix#4010
This commit is contained in:
commit
449367dfe2
36 changed files with 376 additions and 238 deletions
2
.version
2
.version
|
@ -1 +1 @@
|
||||||
25.08b1
|
25.07.3rc1
|
||||||
|
|
|
@ -235,6 +235,8 @@ Sunong2008 <https://github.com/Sunrongguo2008>
|
||||||
Marvin Kopf <marvinkopf@outlook.com>
|
Marvin Kopf <marvinkopf@outlook.com>
|
||||||
Kevin Nakamura <grinkers@grinkers.net>
|
Kevin Nakamura <grinkers@grinkers.net>
|
||||||
Felipe Colona <felipedev202@gmail.com>
|
Felipe Colona <felipedev202@gmail.com>
|
||||||
|
Bradley Szoke <bradleyszoke@gmail.com>
|
||||||
|
jcznk <https://github.com/jcznk>
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ walkdir = "2.5.0"
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
widestring = "1.1.0"
|
widestring = "1.1.0"
|
||||||
winapi = { version = "0.3", features = ["wincon", "winreg"] }
|
winapi = { version = "0.3", features = ["wincon", "winreg"] }
|
||||||
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_Foundation", "Win32_UI_Shell"] }
|
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] }
|
||||||
wiremock = "0.6.3"
|
wiremock = "0.6.3"
|
||||||
xz2 = "0.1.7"
|
xz2 = "0.1.7"
|
||||||
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
|
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
|
||||||
|
|
|
@ -32,10 +32,19 @@ pub fn setup_pyenv(args: PyenvArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut command = Command::new(args.uv_bin);
|
||||||
|
|
||||||
|
// remove UV_* environment variables to avoid interference
|
||||||
|
for (key, _) in std::env::vars() {
|
||||||
|
if key.starts_with("UV_") || key == "VIRTUAL_ENV" {
|
||||||
|
command.env_remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
run_command(
|
run_command(
|
||||||
Command::new(args.uv_bin)
|
command
|
||||||
.env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone())
|
.env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone())
|
||||||
.args(["sync", "--locked"])
|
.args(["sync", "--locked", "--no-config"])
|
||||||
.args(args.extra_args),
|
.args(args.extra_args),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3d04bcbf7fefca0007bc9db307409d88210995d8
|
Subproject commit b90ef6f03c251eb336029ac7c5f551200d41273f
|
|
@ -34,7 +34,7 @@ preferences-when-adding-default-to-current-deck = When adding, default to curren
|
||||||
preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File > Switch Profile.
|
preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File > Switch Profile.
|
||||||
preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14)
|
preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14)
|
||||||
preferences-default-search-text = Default search text
|
preferences-default-search-text = Default search text
|
||||||
preferences-default-search-text-example = eg. 'deck:current '
|
preferences-default-search-text-example = e.g. "deck:current"
|
||||||
preferences-theme = Theme
|
preferences-theme = Theme
|
||||||
preferences-theme-follow-system = Follow System
|
preferences-theme-follow-system = Follow System
|
||||||
preferences-theme-light = Light
|
preferences-theme-light = Light
|
||||||
|
|
|
@ -80,7 +80,7 @@ statistics-reviews =
|
||||||
# This fragment of the tooltip in the FSRS simulation
|
# This fragment of the tooltip in the FSRS simulation
|
||||||
# diagram (Deck options -> FSRS) shows the total number of
|
# diagram (Deck options -> FSRS) shows the total number of
|
||||||
# cards that can be recalled or retrieved on a specific date.
|
# cards that can be recalled or retrieved on a specific date.
|
||||||
statistics-memorized = {$memorized} memorized
|
statistics-memorized = {$memorized} cards memorized
|
||||||
statistics-today-title = Today
|
statistics-today-title = Today
|
||||||
statistics-today-again-count = Again count:
|
statistics-today-again-count = Again count:
|
||||||
statistics-today-type-counts = Learn: { $learnCount }, Review: { $reviewCount }, Relearn: { $relearnCount }, Filtered: { $filteredCount }
|
statistics-today-type-counts = Learn: { $learnCount }, Review: { $reviewCount }, Relearn: { $relearnCount }, Filtered: { $filteredCount }
|
||||||
|
@ -99,9 +99,9 @@ statistics-counts-relearning-cards = Relearning
|
||||||
statistics-counts-title = Card Counts
|
statistics-counts-title = Card Counts
|
||||||
statistics-counts-separate-suspended-buried-cards = Separate suspended/buried cards
|
statistics-counts-separate-suspended-buried-cards = Separate suspended/buried cards
|
||||||
|
|
||||||
## Retention rate represents your actual retention rate from past reviews, in
|
## Retention represents your actual retention from past reviews, in
|
||||||
## comparison to the "desired retention" setting of FSRS, which forecasts
|
## comparison to the "desired retention" setting of FSRS, which forecasts
|
||||||
## future retention. Retention rate is the percentage of all reviewed cards
|
## future retention. Retention is the percentage of all reviewed cards
|
||||||
## that were marked as "Hard," "Good," or "Easy" within a specific time period.
|
## that were marked as "Hard," "Good," or "Easy" within a specific time period.
|
||||||
##
|
##
|
||||||
## Most of these strings are used as column / row headings in a table.
|
## Most of these strings are used as column / row headings in a table.
|
||||||
|
@ -112,9 +112,9 @@ statistics-counts-separate-suspended-buried-cards = Separate suspended/buried ca
|
||||||
## N.B. Stats cards may be very small on mobile devices and when the Stats
|
## N.B. Stats cards may be very small on mobile devices and when the Stats
|
||||||
## window is certain sizes.
|
## window is certain sizes.
|
||||||
|
|
||||||
statistics-true-retention-title = Retention rate
|
statistics-true-retention-title = Retention
|
||||||
statistics-true-retention-subtitle = Pass rate of cards with an interval ≥ 1 day.
|
statistics-true-retention-subtitle = Pass rate of cards with an interval ≥ 1 day.
|
||||||
statistics-true-retention-tooltip = If you are using FSRS, your retention rate is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data.
|
statistics-true-retention-tooltip = If you are using FSRS, your retention is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data.
|
||||||
statistics-true-retention-range = Range
|
statistics-true-retention-range = Range
|
||||||
statistics-true-retention-pass = Pass
|
statistics-true-retention-pass = Pass
|
||||||
statistics-true-retention-fail = Fail
|
statistics-true-retention-fail = Fail
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit c65a9587b1f18931986bdf145872e8e4c44c5c82
|
Subproject commit 9aa63c335c61b30421d39cf43fd8e3975179059c
|
|
@ -404,6 +404,7 @@ message SimulateFsrsReviewRequest {
|
||||||
repeated float easy_days_percentages = 10;
|
repeated float easy_days_percentages = 10;
|
||||||
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
|
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
|
||||||
optional uint32 suspend_after_lapse_count = 12;
|
optional uint32 suspend_after_lapse_count = 12;
|
||||||
|
float historical_retention = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SimulateFsrsReviewResponse {
|
message SimulateFsrsReviewResponse {
|
||||||
|
|
|
@ -7,7 +7,7 @@ dependencies = [
|
||||||
"decorator",
|
"decorator",
|
||||||
"markdown",
|
"markdown",
|
||||||
"orjson",
|
"orjson",
|
||||||
"protobuf>=4.21",
|
"protobuf>=6.0,<8.0",
|
||||||
"requests[socks]",
|
"requests[socks]",
|
||||||
# remove after we update to min python 3.11+
|
# remove after we update to min python 3.11+
|
||||||
"typing_extensions",
|
"typing_extensions",
|
||||||
|
|
|
@ -70,10 +70,10 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
abouttext += f"<p>{lede}"
|
abouttext += f"<p>{lede}"
|
||||||
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
|
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
|
||||||
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
|
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
|
||||||
abouttext += ("Python %s Qt %s PyQt %s<br>") % (
|
abouttext += ("Python %s Qt %s Chromium %s<br>") % (
|
||||||
platform.python_version(),
|
platform.python_version(),
|
||||||
qVersion(),
|
qVersion(),
|
||||||
PYQT_VERSION_STR,
|
(qWebEngineChromiumVersion() or "").split(".")[0],
|
||||||
)
|
)
|
||||||
abouttext += (
|
abouttext += (
|
||||||
without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite))
|
without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite))
|
||||||
|
@ -225,6 +225,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
"Adnane Taghi",
|
"Adnane Taghi",
|
||||||
"Anon_0000",
|
"Anon_0000",
|
||||||
"Bilolbek Normuminov",
|
"Bilolbek Normuminov",
|
||||||
|
"Sagiv Marzini",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1292,9 +1292,10 @@
|
||||||
<tabstop>daily_backups</tabstop>
|
<tabstop>daily_backups</tabstop>
|
||||||
<tabstop>weekly_backups</tabstop>
|
<tabstop>weekly_backups</tabstop>
|
||||||
<tabstop>monthly_backups</tabstop>
|
<tabstop>monthly_backups</tabstop>
|
||||||
<tabstop>tabWidget</tabstop>
|
|
||||||
<tabstop>syncAnkiHubLogout</tabstop>
|
<tabstop>syncAnkiHubLogout</tabstop>
|
||||||
<tabstop>syncAnkiHubLogin</tabstop>
|
<tabstop>syncAnkiHubLogin</tabstop>
|
||||||
|
<tabstop>buttonBox</tabstop>
|
||||||
|
<tabstop>tabWidget</tabstop>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|
|
@ -124,17 +124,14 @@ def launcher_executable() -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def trigger_launcher_run() -> None:
|
def trigger_launcher_run() -> None:
|
||||||
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
"""Create a trigger file to request launcher UI on next run."""
|
||||||
try:
|
try:
|
||||||
root = launcher_root()
|
root = launcher_root()
|
||||||
if not root:
|
if not root:
|
||||||
return
|
return
|
||||||
|
|
||||||
pyproject_path = Path(root) / "pyproject.toml"
|
trigger_path = Path(root) / ".want-launcher"
|
||||||
|
trigger_path.touch()
|
||||||
if pyproject_path.exists():
|
|
||||||
# Touch the file to update its mtime
|
|
||||||
pyproject_path.touch()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
@ -150,6 +147,10 @@ def update_and_restart() -> None:
|
||||||
|
|
||||||
with contextlib.suppress(ResourceWarning):
|
with contextlib.suppress(ResourceWarning):
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
# fixes a bug where launcher fails to appear if opening it
|
||||||
|
# straight after updating
|
||||||
|
if "GNOME_TERMINAL_SCREEN" in env:
|
||||||
|
del env["GNOME_TERMINAL_SCREEN"]
|
||||||
creationflags = 0
|
creationflags = 0
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
creationflags = (
|
creationflags = (
|
||||||
|
|
|
@ -82,11 +82,14 @@ class Preferences(QDialog):
|
||||||
)
|
)
|
||||||
group = self.form.preferences_answer_keys
|
group = self.form.preferences_answer_keys
|
||||||
group.setLayout(layout := QFormLayout())
|
group.setLayout(layout := QFormLayout())
|
||||||
|
tab_widget: QWidget = self.form.url_schemes
|
||||||
for ease, label in ease_labels:
|
for ease, label in ease_labels:
|
||||||
layout.addRow(
|
layout.addRow(
|
||||||
label,
|
label,
|
||||||
line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""),
|
line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""),
|
||||||
)
|
)
|
||||||
|
QWidget.setTabOrder(tab_widget, line_edit)
|
||||||
|
tab_widget = line_edit
|
||||||
qconnect(
|
qconnect(
|
||||||
line_edit.textChanged,
|
line_edit.textChanged,
|
||||||
functools.partial(self.mw.pm.set_answer_key, ease),
|
functools.partial(self.mw.pm.set_answer_key, ease),
|
||||||
|
|
|
@ -177,9 +177,13 @@ class CustomStyles:
|
||||||
QPushButton:default {{
|
QPushButton:default {{
|
||||||
border: 1px solid {tm.var(colors.BORDER_FOCUS)};
|
border: 1px solid {tm.var(colors.BORDER_FOCUS)};
|
||||||
}}
|
}}
|
||||||
|
QPushButton {{
|
||||||
|
margin: 1px;
|
||||||
|
}}
|
||||||
QPushButton:focus {{
|
QPushButton:focus {{
|
||||||
border: 2px solid {tm.var(colors.BORDER_FOCUS)};
|
border: 2px solid {tm.var(colors.BORDER_FOCUS)};
|
||||||
outline: none;
|
outline: none;
|
||||||
|
margin: 0px;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover,
|
QPushButton:hover,
|
||||||
QTabBar::tab:hover,
|
QTabBar::tab:hover,
|
||||||
|
|
|
@ -69,17 +69,14 @@ def add_python_requirements(reqs: list[str]) -> tuple[bool, str]:
|
||||||
|
|
||||||
|
|
||||||
def trigger_launcher_run() -> None:
|
def trigger_launcher_run() -> None:
|
||||||
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
"""Create a trigger file to request launcher UI on next run."""
|
||||||
try:
|
try:
|
||||||
root = launcher_root()
|
root = launcher_root()
|
||||||
if not root:
|
if not root:
|
||||||
return
|
return
|
||||||
|
|
||||||
pyproject_path = Path(root) / "pyproject.toml"
|
trigger_path = Path(root) / ".want-launcher"
|
||||||
|
trigger_path.touch()
|
||||||
if pyproject_path.exists():
|
|
||||||
# Touch the file to update its mtime
|
|
||||||
pyproject_path.touch()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ HOST_ARCH=$(uname -m)
|
||||||
|
|
||||||
# Define output paths
|
# Define output paths
|
||||||
OUTPUT_DIR="../../../out/launcher"
|
OUTPUT_DIR="../../../out/launcher"
|
||||||
LAUNCHER_DIR="$OUTPUT_DIR/anki-linux"
|
ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
|
||||||
|
LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux"
|
||||||
|
|
||||||
# Clean existing output directory
|
# Clean existing output directory
|
||||||
rm -rf "$LAUNCHER_DIR"
|
rm -rf "$LAUNCHER_DIR"
|
||||||
|
@ -77,8 +78,8 @@ chmod +x \
|
||||||
chmod -R a+r "$LAUNCHER_DIR"
|
chmod -R a+r "$LAUNCHER_DIR"
|
||||||
|
|
||||||
ZSTD="zstd -c --long -T0 -18"
|
ZSTD="zstd -c --long -T0 -18"
|
||||||
TRANSFORM="s%^.%anki-linux%S"
|
TRANSFORM="s%^.%anki-launcher-$ANKI_VERSION-linux%S"
|
||||||
TARBALL="$OUTPUT_DIR/anki-linux.tar.zst"
|
TARBALL="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux.tar.zst"
|
||||||
|
|
||||||
tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" .
|
tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" .
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Anki</string>
|
<string>Anki</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>ANKI_VERSION</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12</string>
|
<string>12</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
|
|
|
@ -31,7 +31,8 @@ lipo -create \
|
||||||
cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/"
|
cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/"
|
||||||
|
|
||||||
# Copy support files
|
# Copy support files
|
||||||
cp Info.plist "$APP_LAUNCHER/Contents/"
|
ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
|
||||||
|
sed "s/ANKI_VERSION/$ANKI_VERSION/g" Info.plist > "$APP_LAUNCHER/Contents/Info.plist"
|
||||||
cp icon/Assets.car "$APP_LAUNCHER/Contents/Resources/"
|
cp icon/Assets.car "$APP_LAUNCHER/Contents/Resources/"
|
||||||
cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
|
cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
|
||||||
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
|
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
|
||||||
|
|
|
@ -6,7 +6,8 @@ set -e
|
||||||
# base folder with Anki.app in it
|
# base folder with Anki.app in it
|
||||||
output="$1"
|
output="$1"
|
||||||
dist="$1/tmp"
|
dist="$1/tmp"
|
||||||
dmg_path="$output/Anki.dmg"
|
ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
|
||||||
|
dmg_path="$output/anki-launcher-$ANKI_VERSION-mac.dmg"
|
||||||
|
|
||||||
if [ -d "/Volumes/Anki" ]
|
if [ -d "/Volumes/Anki" ]
|
||||||
then
|
then
|
||||||
|
|
|
@ -22,6 +22,11 @@ const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe";
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
println!("Building Windows launcher...");
|
println!("Building Windows launcher...");
|
||||||
|
|
||||||
|
// Read version early so it can be used throughout the build process
|
||||||
|
let version = std::fs::read_to_string("../../../.version")?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let output_dir = PathBuf::from(OUTPUT_DIR);
|
let output_dir = PathBuf::from(OUTPUT_DIR);
|
||||||
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
|
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
|
||||||
let nsis_dir = PathBuf::from(NSIS_DIR);
|
let nsis_dir = PathBuf::from(NSIS_DIR);
|
||||||
|
@ -31,16 +36,20 @@ fn main() -> Result<()> {
|
||||||
extract_nsis_plugins()?;
|
extract_nsis_plugins()?;
|
||||||
copy_files(&output_dir)?;
|
copy_files(&output_dir)?;
|
||||||
sign_binaries(&output_dir)?;
|
sign_binaries(&output_dir)?;
|
||||||
copy_nsis_files(&nsis_dir)?;
|
copy_nsis_files(&nsis_dir, &version)?;
|
||||||
build_uninstaller(&output_dir, &nsis_dir)?;
|
build_uninstaller(&output_dir, &nsis_dir)?;
|
||||||
sign_file(&output_dir.join("uninstall.exe"))?;
|
sign_file(&output_dir.join("uninstall.exe"))?;
|
||||||
generate_install_manifest(&output_dir)?;
|
generate_install_manifest(&output_dir)?;
|
||||||
build_installer(&output_dir, &nsis_dir)?;
|
build_installer(&output_dir, &nsis_dir)?;
|
||||||
sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?;
|
|
||||||
|
let installer_filename = format!("anki-launcher-{version}-windows.exe");
|
||||||
|
let installer_path = PathBuf::from("../../../out/launcher_exe").join(&installer_filename);
|
||||||
|
|
||||||
|
sign_file(&installer_path)?;
|
||||||
|
|
||||||
println!("Build completed successfully!");
|
println!("Build completed successfully!");
|
||||||
println!("Output directory: {}", output_dir.display());
|
println!("Output directory: {}", output_dir.display());
|
||||||
println!("Installer: ../../../out/launcher_exe/anki-install.exe");
|
println!("Installer: ../../../out/launcher_exe/{installer_filename}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -235,11 +244,13 @@ fn generate_install_manifest(output_dir: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_nsis_files(nsis_dir: &Path) -> Result<()> {
|
fn copy_nsis_files(nsis_dir: &Path, version: &str) -> Result<()> {
|
||||||
println!("Copying NSIS support files...");
|
println!("Copying NSIS support files...");
|
||||||
|
|
||||||
// Copy anki.template.nsi as anki.nsi
|
// Copy anki.template.nsi as anki.nsi and substitute version placeholders
|
||||||
copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?;
|
let template_content = std::fs::read_to_string("anki.template.nsi")?;
|
||||||
|
let substituted_content = template_content.replace("ANKI_VERSION", version);
|
||||||
|
write_file(nsis_dir.join("anki.nsi"), substituted_content)?;
|
||||||
|
|
||||||
// Copy fileassoc.nsh
|
// Copy fileassoc.nsh
|
||||||
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;
|
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;
|
||||||
|
|
|
@ -46,6 +46,8 @@ struct State {
|
||||||
dist_python_version_path: std::path::PathBuf,
|
dist_python_version_path: std::path::PathBuf,
|
||||||
uv_lock_path: std::path::PathBuf,
|
uv_lock_path: std::path::PathBuf,
|
||||||
sync_complete_marker: std::path::PathBuf,
|
sync_complete_marker: std::path::PathBuf,
|
||||||
|
launcher_trigger_file: std::path::PathBuf,
|
||||||
|
pyproject_modified_by_user: bool,
|
||||||
previous_version: Option<String>,
|
previous_version: Option<String>,
|
||||||
resources_dir: std::path::PathBuf,
|
resources_dir: std::path::PathBuf,
|
||||||
}
|
}
|
||||||
|
@ -56,6 +58,12 @@ pub enum VersionKind {
|
||||||
Uv(String),
|
Uv(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Releases {
|
||||||
|
pub latest: Vec<String>,
|
||||||
|
pub all: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum MainMenuChoice {
|
pub enum MainMenuChoice {
|
||||||
Latest,
|
Latest,
|
||||||
|
@ -64,7 +72,6 @@ pub enum MainMenuChoice {
|
||||||
ToggleBetas,
|
ToggleBetas,
|
||||||
ToggleCache,
|
ToggleCache,
|
||||||
Uninstall,
|
Uninstall,
|
||||||
Quit,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -100,6 +107,8 @@ fn run() -> Result<()> {
|
||||||
dist_python_version_path: resources_dir.join(".python-version"),
|
dist_python_version_path: resources_dir.join(".python-version"),
|
||||||
uv_lock_path: uv_install_root.join("uv.lock"),
|
uv_lock_path: uv_install_root.join("uv.lock"),
|
||||||
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
||||||
|
launcher_trigger_file: uv_install_root.join(".want-launcher"),
|
||||||
|
pyproject_modified_by_user: false, // calculated later
|
||||||
previous_version: None,
|
previous_version: None,
|
||||||
resources_dir,
|
resources_dir,
|
||||||
};
|
};
|
||||||
|
@ -119,15 +128,15 @@ fn run() -> Result<()> {
|
||||||
&state.user_python_version_path,
|
&state.user_python_version_path,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pyproject_has_changed = !state.sync_complete_marker.exists() || {
|
let launcher_requested = state.launcher_trigger_file.exists();
|
||||||
let pyproject_toml_time = modified_time(&state.user_pyproject_path)?;
|
|
||||||
let sync_complete_time = modified_time(&state.sync_complete_marker)?;
|
|
||||||
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
|
|
||||||
}
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if !pyproject_has_changed {
|
// Calculate whether user has custom edits that need syncing
|
||||||
// If venv is already up to date, launch Anki normally
|
let pyproject_time = file_timestamp_secs(&state.user_pyproject_path);
|
||||||
|
let sync_time = file_timestamp_secs(&state.sync_complete_marker);
|
||||||
|
state.pyproject_modified_by_user = pyproject_time > sync_time;
|
||||||
|
let pyproject_has_changed = state.pyproject_modified_by_user;
|
||||||
|
if !launcher_requested && !pyproject_has_changed {
|
||||||
|
// If no launcher request and venv is already up to date, launch Anki normally
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let cmd = build_python_command(&state, &args)?;
|
let cmd = build_python_command(&state, &args)?;
|
||||||
launch_anki_normally(cmd)?;
|
launch_anki_normally(cmd)?;
|
||||||
|
@ -137,6 +146,11 @@ fn run() -> Result<()> {
|
||||||
// If we weren't in a terminal, respawn ourselves in one
|
// If we weren't in a terminal, respawn ourselves in one
|
||||||
ensure_terminal_shown()?;
|
ensure_terminal_shown()?;
|
||||||
|
|
||||||
|
if launcher_requested {
|
||||||
|
// Remove the trigger file to make request ephemeral
|
||||||
|
let _ = remove_file(&state.launcher_trigger_file);
|
||||||
|
}
|
||||||
|
|
||||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||||
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
||||||
|
|
||||||
|
@ -184,6 +198,7 @@ fn extract_aqt_version(
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let output = Command::new(uv_path)
|
let output = Command::new(uv_path)
|
||||||
.current_dir(uv_install_root)
|
.current_dir(uv_install_root)
|
||||||
|
.env("VIRTUAL_ENV", uv_install_root.join(".venv"))
|
||||||
.args(["pip", "show", "aqt"])
|
.args(["pip", "show", "aqt"])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
@ -230,13 +245,7 @@ fn check_versions(state: &mut State) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Result<()> {
|
fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Result<()> {
|
||||||
update_pyproject_for_version(
|
update_pyproject_for_version(choice.clone(), state)?;
|
||||||
choice.clone(),
|
|
||||||
state.dist_pyproject_path.clone(),
|
|
||||||
state.user_pyproject_path.clone(),
|
|
||||||
state.dist_python_version_path.clone(),
|
|
||||||
state.user_python_version_path.clone(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Extract current version before syncing (but don't write to file yet)
|
// Extract current version before syncing (but don't write to file yet)
|
||||||
let previous_version_to_save = extract_aqt_version(&state.uv_path, &state.uv_install_root);
|
let previous_version_to_save = extract_aqt_version(&state.uv_path, &state.uv_install_root);
|
||||||
|
@ -255,40 +264,27 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// `uv sync` sometimes does not pull in Python automatically
|
// Prepare to sync the venv
|
||||||
// This might be system/platform specific and/or a uv bug.
|
|
||||||
let mut command = Command::new(&state.uv_path);
|
let mut command = Command::new(&state.uv_path);
|
||||||
command
|
command.current_dir(&state.uv_install_root);
|
||||||
.current_dir(&state.uv_install_root)
|
|
||||||
.env("UV_CACHE_DIR", &state.uv_cache_dir)
|
|
||||||
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
|
|
||||||
.args(["python", "install", "--managed-python"]);
|
|
||||||
|
|
||||||
// Add python version if .python-version file exists
|
// remove UV_* environment variables to avoid interference
|
||||||
if let Some(version) = &python_version_trimmed {
|
for (key, _) in std::env::vars() {
|
||||||
command.args([version]);
|
if key.starts_with("UV_") || key == "VIRTUAL_ENV" {
|
||||||
|
command.env_remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command.ensure_success().context("Python install failed")?;
|
|
||||||
|
|
||||||
// Sync the venv
|
|
||||||
let mut command = Command::new(&state.uv_path);
|
|
||||||
command
|
command
|
||||||
.current_dir(&state.uv_install_root)
|
|
||||||
.env("UV_CACHE_DIR", &state.uv_cache_dir)
|
.env("UV_CACHE_DIR", &state.uv_cache_dir)
|
||||||
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
|
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
|
||||||
.args(["sync", "--upgrade", "--managed-python"]);
|
.args(["sync", "--upgrade", "--managed-python", "--no-config"]);
|
||||||
|
|
||||||
// Add python version if .python-version file exists
|
// Add python version if .python-version file exists
|
||||||
if let Some(version) = &python_version_trimmed {
|
if let Some(version) = &python_version_trimmed {
|
||||||
command.args(["--python", version]);
|
command.args(["--python", version]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set UV_PRERELEASE=allow if beta mode is enabled
|
|
||||||
if state.prerelease_marker.exists() {
|
|
||||||
command.env("UV_PRERELEASE", "allow");
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.no_cache_marker.exists() {
|
if state.no_cache_marker.exists() {
|
||||||
command.env("UV_NO_CACHE", "1");
|
command.env("UV_NO_CACHE", "1");
|
||||||
}
|
}
|
||||||
|
@ -326,9 +322,11 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
||||||
let menu_choice = get_main_menu_choice(state)?;
|
let menu_choice = get_main_menu_choice(state)?;
|
||||||
|
|
||||||
match menu_choice {
|
match menu_choice {
|
||||||
MainMenuChoice::Quit => std::process::exit(0),
|
|
||||||
MainMenuChoice::KeepExisting => {
|
MainMenuChoice::KeepExisting => {
|
||||||
// Skip sync, just launch existing installation
|
if state.pyproject_modified_by_user {
|
||||||
|
// User has custom edits, sync them
|
||||||
|
handle_version_install_or_update(state, MainMenuChoice::KeepExisting)?;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
MainMenuChoice::ToggleBetas => {
|
MainMenuChoice::ToggleBetas => {
|
||||||
|
@ -385,14 +383,28 @@ fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get mtime of provided file, or 0 if unavailable
|
||||||
|
fn file_timestamp_secs(path: &std::path::Path) -> i64 {
|
||||||
|
modified_time(path)
|
||||||
|
.map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
loop {
|
loop {
|
||||||
println!("1) Latest Anki (just press enter)");
|
println!("1) Latest Anki (press Enter)");
|
||||||
println!("2) Choose a version");
|
println!("2) Choose a version");
|
||||||
|
|
||||||
if let Some(current_version) = &state.current_version {
|
if let Some(current_version) = &state.current_version {
|
||||||
let normalized_current = normalize_version(current_version);
|
let normalized_current = normalize_version(current_version);
|
||||||
|
|
||||||
|
if state.pyproject_modified_by_user {
|
||||||
|
println!("3) Sync project changes");
|
||||||
|
} else {
|
||||||
println!("3) Keep existing version ({normalized_current})");
|
println!("3) Keep existing version ({normalized_current})");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(prev_version) = &state.previous_version {
|
if let Some(prev_version) = &state.previous_version {
|
||||||
if state.current_version.as_ref() != Some(prev_version) {
|
if state.current_version.as_ref() != Some(prev_version) {
|
||||||
let normalized_prev = normalize_version(prev_version);
|
let normalized_prev = normalize_version(prev_version);
|
||||||
|
@ -413,7 +425,6 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
println!("7) Uninstall");
|
println!("7) Uninstall");
|
||||||
println!("8) Quit");
|
|
||||||
print!("> ");
|
print!("> ");
|
||||||
let _ = stdout().flush();
|
let _ = stdout().flush();
|
||||||
|
|
||||||
|
@ -453,7 +464,6 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
"5" => MainMenuChoice::ToggleBetas,
|
"5" => MainMenuChoice::ToggleBetas,
|
||||||
"6" => MainMenuChoice::ToggleCache,
|
"6" => MainMenuChoice::ToggleCache,
|
||||||
"7" => MainMenuChoice::Uninstall,
|
"7" => MainMenuChoice::Uninstall,
|
||||||
"8" => MainMenuChoice::Quit,
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("Invalid input. Please try again.");
|
println!("Invalid input. Please try again.");
|
||||||
continue;
|
continue;
|
||||||
|
@ -465,13 +475,9 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
println!("Please wait...");
|
println!("Please wait...");
|
||||||
|
|
||||||
let include_prereleases = state.prerelease_marker.exists();
|
let releases = get_releases(state)?;
|
||||||
let all_versions = fetch_versions(state)?;
|
let releases_str = releases
|
||||||
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
.latest
|
||||||
|
|
||||||
let latest_patches = with_only_latest_patch(&all_versions);
|
|
||||||
let latest_releases: Vec<&String> = latest_patches.iter().take(5).collect();
|
|
||||||
let releases_str = latest_releases
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| v.as_str())
|
.map(|v| v.as_str())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -494,7 +500,7 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
let normalized_input = normalize_version(input);
|
let normalized_input = normalize_version(input);
|
||||||
|
|
||||||
// Check if the version exists in the available versions
|
// Check if the version exists in the available versions
|
||||||
let version_exists = all_versions.iter().any(|v| v == &normalized_input);
|
let version_exists = releases.all.iter().any(|v| v == &normalized_input);
|
||||||
|
|
||||||
match (parse_version_kind(input), version_exists) {
|
match (parse_version_kind(input), version_exists) {
|
||||||
(Some(version_kind), true) => {
|
(Some(version_kind), true) => {
|
||||||
|
@ -502,7 +508,7 @@ fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
Ok(Some(version_kind))
|
Ok(Some(version_kind))
|
||||||
}
|
}
|
||||||
(None, true) => {
|
(None, true) => {
|
||||||
println!("Versions before 2.1.50 can't be installedn");
|
println!("Versions before 2.1.50 can't be installed.");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -635,37 +641,23 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
|
||||||
Ok(versions)
|
Ok(versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_pyproject_for_version(
|
fn get_releases(state: &State) -> Result<Releases> {
|
||||||
menu_choice: MainMenuChoice,
|
let include_prereleases = state.prerelease_marker.exists();
|
||||||
dist_pyproject_path: std::path::PathBuf,
|
let all_versions = fetch_versions(state)?;
|
||||||
user_pyproject_path: std::path::PathBuf,
|
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
||||||
dist_python_version_path: std::path::PathBuf,
|
|
||||||
user_python_version_path: std::path::PathBuf,
|
let latest_patches = with_only_latest_patch(&all_versions);
|
||||||
) -> Result<()> {
|
let latest_releases: Vec<String> = latest_patches.into_iter().take(5).collect();
|
||||||
match menu_choice {
|
Ok(Releases {
|
||||||
MainMenuChoice::Latest => {
|
latest: latest_releases,
|
||||||
let content = read_file(&dist_pyproject_path)?;
|
all: all_versions,
|
||||||
write_file(&user_pyproject_path, &content)?;
|
})
|
||||||
let python_version_content = read_file(&dist_python_version_path)?;
|
}
|
||||||
write_file(&user_python_version_path, &python_version_content)?;
|
|
||||||
}
|
fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> {
|
||||||
MainMenuChoice::KeepExisting => {
|
let content = read_file(&state.dist_pyproject_path)?;
|
||||||
// Do nothing - keep existing pyproject.toml and .python-version
|
let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
|
||||||
}
|
let updated_content = match version_kind {
|
||||||
MainMenuChoice::ToggleBetas => {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
MainMenuChoice::ToggleCache => {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
MainMenuChoice::Uninstall => {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
MainMenuChoice::Version(version_kind) => {
|
|
||||||
let content = read_file(&dist_pyproject_path)?;
|
|
||||||
let content_str =
|
|
||||||
String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?;
|
|
||||||
let updated_content = match &version_kind {
|
|
||||||
VersionKind::PyOxidizer(version) => {
|
VersionKind::PyOxidizer(version) => {
|
||||||
// Replace package name and add PyQt6 dependencies
|
// Replace package name and add PyQt6 dependencies
|
||||||
content_str.replace(
|
content_str.replace(
|
||||||
|
@ -684,24 +676,50 @@ fn update_pyproject_for_version(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
VersionKind::Uv(version) => {
|
VersionKind::Uv(version) => content_str.replace(
|
||||||
content_str.replace("anki-release", &format!("anki-release=={version}"))
|
"anki-release",
|
||||||
}
|
&format!("anki-release=={version}\",\n \"anki=={version}\",\n \"aqt=={version}"),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
write_file(&user_pyproject_path, &updated_content)?;
|
write_file(&state.user_pyproject_path, &updated_content)?;
|
||||||
|
|
||||||
// Update .python-version based on version kind
|
// Update .python-version based on version kind
|
||||||
match &version_kind {
|
match version_kind {
|
||||||
VersionKind::PyOxidizer(_) => {
|
VersionKind::PyOxidizer(_) => {
|
||||||
write_file(&user_python_version_path, "3.9")?;
|
write_file(&state.user_python_version_path, "3.9")?;
|
||||||
}
|
}
|
||||||
VersionKind::Uv(_) => {
|
VersionKind::Uv(_) => {
|
||||||
copy_file(&dist_python_version_path, &user_python_version_path)?;
|
copy_file(
|
||||||
|
&state.dist_python_version_path,
|
||||||
|
&state.user_python_version_path,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> Result<()> {
|
||||||
|
match menu_choice {
|
||||||
|
MainMenuChoice::Latest => {
|
||||||
|
// Get the latest release version and create a VersionKind for it
|
||||||
|
let releases = get_releases(state)?;
|
||||||
|
let latest_version = releases.latest.first().context("No latest version found")?;
|
||||||
|
apply_version_kind(&VersionKind::Uv(latest_version.clone()), state)?;
|
||||||
}
|
}
|
||||||
MainMenuChoice::Quit => {
|
MainMenuChoice::KeepExisting => {
|
||||||
std::process::exit(0);
|
// Do nothing - keep existing pyproject.toml and .python-version
|
||||||
|
}
|
||||||
|
MainMenuChoice::ToggleBetas => {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
MainMenuChoice::ToggleCache => {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
MainMenuChoice::Uninstall => {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
MainMenuChoice::Version(version_kind) => {
|
||||||
|
apply_version_kind(&version_kind, state)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -875,11 +893,6 @@ fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
|
||||||
cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str());
|
cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str());
|
||||||
cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str());
|
cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str());
|
||||||
|
|
||||||
// Set UV_PRERELEASE=allow if beta mode is enabled
|
|
||||||
if state.prerelease_marker.exists() {
|
|
||||||
cmd.env("UV_PRERELEASE", "allow");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,21 +11,22 @@ pub fn relaunch_in_terminal() -> Result<()> {
|
||||||
|
|
||||||
// Try terminals in roughly most specific to least specific.
|
// Try terminals in roughly most specific to least specific.
|
||||||
// First, try commonly used terminals for riced systems.
|
// First, try commonly used terminals for riced systems.
|
||||||
// Second, try the minimalist/compatibility terminals.
|
// Second, try common defaults.
|
||||||
// Finally, try terminals usually installed by default.
|
// Finally, try x11 compatibility terminals.
|
||||||
let terminals = [
|
let terminals = [
|
||||||
// commonly used for riced systems
|
// commonly used for riced systems
|
||||||
("alacritty", vec!["-e"]),
|
("alacritty", vec!["-e"]),
|
||||||
("kitty", vec![]),
|
("kitty", vec![]),
|
||||||
// minimalistic terminals for constrained systems
|
|
||||||
("foot", vec![]),
|
("foot", vec![]),
|
||||||
("urxvt", vec!["-e"]),
|
// the user's default terminal in Debian/Ubuntu
|
||||||
("xterm", vec!["-e"]),
|
|
||||||
("x-terminal-emulator", vec!["-e"]),
|
("x-terminal-emulator", vec!["-e"]),
|
||||||
// default installs for the most common distros
|
// default installs for the most common distros
|
||||||
("xfce4-terminal", vec!["-e"]),
|
("xfce4-terminal", vec!["-e"]),
|
||||||
("gnome-terminal", vec!["--"]),
|
("gnome-terminal", vec!["-e"]),
|
||||||
("konsole", vec!["-e"]),
|
("konsole", vec!["-e"]),
|
||||||
|
// x11-compatibility terminals
|
||||||
|
("urxvt", vec!["-e"]),
|
||||||
|
("xterm", vec!["-e"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (terminal_cmd, args) in &terminals {
|
for (terminal_cmd, args) in &terminals {
|
||||||
|
|
|
@ -8,6 +8,7 @@ use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use widestring::u16cstr;
|
use widestring::u16cstr;
|
||||||
use windows::core::PCWSTR;
|
use windows::core::PCWSTR;
|
||||||
|
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||||||
use windows::Win32::System::Console::AttachConsole;
|
use windows::Win32::System::Console::AttachConsole;
|
||||||
use windows::Win32::System::Console::GetConsoleWindow;
|
use windows::Win32::System::Console::GetConsoleWindow;
|
||||||
use windows::Win32::System::Console::ATTACH_PARENT_PROCESS;
|
use windows::Win32::System::Console::ATTACH_PARENT_PROCESS;
|
||||||
|
@ -18,8 +19,25 @@ use windows::Win32::System::Registry::HKEY;
|
||||||
use windows::Win32::System::Registry::HKEY_CURRENT_USER;
|
use windows::Win32::System::Registry::HKEY_CURRENT_USER;
|
||||||
use windows::Win32::System::Registry::KEY_READ;
|
use windows::Win32::System::Registry::KEY_READ;
|
||||||
use windows::Win32::System::Registry::REG_SZ;
|
use windows::Win32::System::Registry::REG_SZ;
|
||||||
|
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
|
||||||
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||||
|
|
||||||
|
/// Returns true if running on Windows 10 (not Windows 11)
|
||||||
|
fn is_windows_10() -> bool {
|
||||||
|
unsafe {
|
||||||
|
let mut info = OSVERSIONINFOW {
|
||||||
|
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if RtlGetVersion(&mut info).is_ok() {
|
||||||
|
// Windows 10 has build numbers < 22000, Windows 11 >= 22000
|
||||||
|
info.dwBuildNumber < 22000 && info.dwMajorVersion == 10
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ensure_terminal_shown() -> Result<()> {
|
pub fn ensure_terminal_shown() -> Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
if !GetConsoleWindow().is_invalid() {
|
if !GetConsoleWindow().is_invalid() {
|
||||||
|
@ -29,6 +47,14 @@ pub fn ensure_terminal_shown() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() {
|
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() {
|
||||||
|
// This black magic triggers Windows to switch to the new
|
||||||
|
// ANSI-supporting console host, which is usually only available
|
||||||
|
// when the app is built with the console subsystem.
|
||||||
|
// Only needed on Windows 10, not Windows 11.
|
||||||
|
if is_windows_10() {
|
||||||
|
let _ = Command::new("cmd").args(["/C", ""]).status();
|
||||||
|
}
|
||||||
|
|
||||||
// Successfully attached to parent console
|
// Successfully attached to parent console
|
||||||
reconnect_stdio_to_console();
|
reconnect_stdio_to_console();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -24,7 +24,7 @@ Name "Anki"
|
||||||
Unicode true
|
Unicode true
|
||||||
|
|
||||||
; The file to write (relative to nsis directory)
|
; The file to write (relative to nsis directory)
|
||||||
OutFile "..\launcher_exe\anki-install.exe"
|
OutFile "..\launcher_exe\anki-launcher-ANKI_VERSION-windows.exe"
|
||||||
|
|
||||||
; Non elevated
|
; Non elevated
|
||||||
RequestExecutionLevel user
|
RequestExecutionLevel user
|
||||||
|
@ -214,7 +214,7 @@ Section ""
|
||||||
|
|
||||||
; Write the uninstall keys for Windows
|
; Write the uninstall keys for Windows
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher"
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher"
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "1.0.0"
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "ANKI_VERSION"
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
|
||||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1
|
||||||
|
|
|
@ -40,8 +40,8 @@ qt67 = [
|
||||||
qt = [
|
qt = [
|
||||||
"pyqt6==6.9.1",
|
"pyqt6==6.9.1",
|
||||||
"pyqt6-qt6==6.9.1",
|
"pyqt6-qt6==6.9.1",
|
||||||
"pyqt6-webengine==6.9.0",
|
"pyqt6-webengine==6.8.0",
|
||||||
"pyqt6-webengine-qt6==6.9.1",
|
"pyqt6-webengine-qt6==6.8.2",
|
||||||
"pyqt6_sip==13.10.2",
|
"pyqt6_sip==13.10.2",
|
||||||
]
|
]
|
||||||
qt68 = [
|
qt68 = [
|
||||||
|
|
|
@ -228,28 +228,31 @@ impl Collection {
|
||||||
/// Return the next states that will be applied for each answer button.
|
/// Return the next states that will be applied for each answer button.
|
||||||
pub fn get_scheduling_states(&mut self, cid: CardId) -> Result<SchedulingStates> {
|
pub fn get_scheduling_states(&mut self, cid: CardId) -> Result<SchedulingStates> {
|
||||||
let card = self.storage.get_card(cid)?.or_not_found(cid)?;
|
let card = self.storage.get_card(cid)?.or_not_found(cid)?;
|
||||||
let deck = self.get_deck(card.deck_id)?.or_not_found(card.deck_id)?;
|
let note_id = card.note_id;
|
||||||
|
|
||||||
let note_id = deck
|
|
||||||
.config_id()
|
|
||||||
.map(|deck_config_id| self.get_deck_config(deck_config_id, false))
|
|
||||||
.transpose()?
|
|
||||||
.flatten()
|
|
||||||
.map(|deck_config| deck_config.inner.bury_reviews)
|
|
||||||
.unwrap_or(false)
|
|
||||||
.then_some(card.note_id);
|
|
||||||
|
|
||||||
let ctx = self.card_state_updater(card)?;
|
let ctx = self.card_state_updater(card)?;
|
||||||
let current = ctx.current_card_state();
|
let current = ctx.current_card_state();
|
||||||
|
|
||||||
let load_balancer_ctx = self.state.card_queues.as_ref().and_then(|card_queues| {
|
let load_balancer_ctx = if let Some(load_balancer) = self
|
||||||
match card_queues.load_balancer.as_ref() {
|
.state
|
||||||
None => None,
|
.card_queues
|
||||||
Some(load_balancer) => {
|
.as_ref()
|
||||||
Some(load_balancer.review_context(note_id, deck.config_id()?))
|
.and_then(|card_queues| card_queues.load_balancer.as_ref())
|
||||||
|
{
|
||||||
|
// Only get_deck_config when load balancer is enabled
|
||||||
|
if let Some(deck_config_id) = ctx.deck.config_id() {
|
||||||
|
let note_id = self
|
||||||
|
.get_deck_config(deck_config_id, false)?
|
||||||
|
.map(|deck_config| deck_config.inner.bury_reviews)
|
||||||
|
.unwrap_or(false)
|
||||||
|
.then_some(note_id);
|
||||||
|
Some(load_balancer.review_context(note_id, deck_config_id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
});
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let state_ctx = ctx.state_context(load_balancer_ctx);
|
let state_ctx = ctx.state_context(load_balancer_ctx);
|
||||||
Ok(current.next_states(&state_ctx))
|
Ok(current.next_states(&state_ctx))
|
||||||
|
@ -334,6 +337,7 @@ impl Collection {
|
||||||
self.update_deck_stats_from_answer(usn, answer, &updater, original.queue)?;
|
self.update_deck_stats_from_answer(usn, answer, &updater, original.queue)?;
|
||||||
self.maybe_bury_siblings(&original, &updater.config)?;
|
self.maybe_bury_siblings(&original, &updater.config)?;
|
||||||
let timing = updater.timing;
|
let timing = updater.timing;
|
||||||
|
let deckconfig_id = updater.deck.config_id();
|
||||||
let mut card = updater.into_card();
|
let mut card = updater.into_card();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
answer.current_state,
|
answer.current_state,
|
||||||
|
@ -352,15 +356,17 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if card.queue == CardQueue::Review {
|
if card.queue == CardQueue::Review {
|
||||||
let deck = self.get_deck(card.deck_id)?;
|
if let Some(load_balancer) = self
|
||||||
if let Some(card_queues) = self.state.card_queues.as_mut() {
|
.state
|
||||||
if let Some(deckconfig_id) = deck.and_then(|deck| deck.config_id()) {
|
.card_queues
|
||||||
if let Some(load_balancer) = card_queues.load_balancer.as_mut() {
|
.as_mut()
|
||||||
|
.and_then(|card_queues| card_queues.load_balancer.as_mut())
|
||||||
|
{
|
||||||
|
if let Some(deckconfig_id) = deckconfig_id {
|
||||||
load_balancer.add_card(card.id, card.note_id, deckconfig_id, card.interval)
|
load_balancer.add_card(card.id, card.note_id, deckconfig_id, card.interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle queue updates based on from_queue flag
|
// Handle queue updates based on from_queue flag
|
||||||
if answer.from_queue {
|
if answer.from_queue {
|
||||||
|
|
|
@ -377,6 +377,7 @@ pub(crate) fn fsrs_item_for_memory_state(
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// no revlogs (new card or caused by ignore_revlogs_before or deleted revlogs)
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,14 @@ use fsrs::simulate;
|
||||||
use fsrs::PostSchedulingFn;
|
use fsrs::PostSchedulingFn;
|
||||||
use fsrs::ReviewPriorityFn;
|
use fsrs::ReviewPriorityFn;
|
||||||
use fsrs::SimulatorConfig;
|
use fsrs::SimulatorConfig;
|
||||||
|
use fsrs::FSRS;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
use crate::card::CardQueue;
|
use crate::card::CardQueue;
|
||||||
|
use crate::card::CardType;
|
||||||
|
use crate::card::FsrsMemoryState;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::scheduler::states::fuzz::constrained_fuzz_bounds;
|
use crate::scheduler::states::fuzz::constrained_fuzz_bounds;
|
||||||
use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers;
|
use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers;
|
||||||
|
@ -129,7 +132,7 @@ impl Collection {
|
||||||
fn is_included_card(c: &Card) -> bool {
|
fn is_included_card(c: &Card) -> bool {
|
||||||
c.queue != CardQueue::Suspended
|
c.queue != CardQueue::Suspended
|
||||||
&& c.queue != CardQueue::PreviewRepeat
|
&& c.queue != CardQueue::PreviewRepeat
|
||||||
&& c.queue != CardQueue::New
|
&& c.ctype != CardType::New
|
||||||
}
|
}
|
||||||
// calculate any missing memory state
|
// calculate any missing memory state
|
||||||
for c in &mut cards {
|
for c in &mut cards {
|
||||||
|
@ -143,13 +146,29 @@ impl Collection {
|
||||||
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
|
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
|
||||||
let new_cards = cards
|
let new_cards = cards
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.memory_state.is_none() || c.queue == CardQueue::New)
|
.filter(|c| c.ctype == CardType::New && c.queue != CardQueue::Suspended)
|
||||||
.count()
|
.count()
|
||||||
+ req.deck_size as usize;
|
+ req.deck_size as usize;
|
||||||
|
let fsrs = FSRS::new(Some(&req.params))?;
|
||||||
let mut converted_cards = cards
|
let mut converted_cards = cards
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(is_included_card)
|
.filter(is_included_card)
|
||||||
.filter_map(|c| Card::convert(c, days_elapsed))
|
.filter_map(|c| {
|
||||||
|
let memory_state = match c.memory_state {
|
||||||
|
Some(state) => state,
|
||||||
|
// cards that lack memory states after compute_memory_state have no FSRS items,
|
||||||
|
// implying a truncated or ignored revlog
|
||||||
|
None => fsrs
|
||||||
|
.memory_state_from_sm2(
|
||||||
|
c.ease_factor(),
|
||||||
|
c.interval as f32,
|
||||||
|
req.historical_retention,
|
||||||
|
)
|
||||||
|
.ok()?
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
Card::convert(c, days_elapsed, memory_state)
|
||||||
|
})
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
let introduced_today_count = self
|
let introduced_today_count = self
|
||||||
.search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)?
|
.search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)?
|
||||||
|
@ -251,17 +270,16 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Card {
|
impl Card {
|
||||||
fn convert(card: Card, days_elapsed: i32) -> Option<fsrs::Card> {
|
fn convert(card: Card, days_elapsed: i32, memory_state: FsrsMemoryState) -> Option<fsrs::Card> {
|
||||||
match card.memory_state {
|
match card.queue {
|
||||||
Some(state) => match card.queue {
|
|
||||||
CardQueue::DayLearn | CardQueue::Review => {
|
CardQueue::DayLearn | CardQueue::Review => {
|
||||||
let due = card.original_or_current_due();
|
let due = card.original_or_current_due();
|
||||||
let relative_due = due - days_elapsed;
|
let relative_due = due - days_elapsed;
|
||||||
let last_date = (relative_due - card.interval as i32).min(0) as f32;
|
let last_date = (relative_due - card.interval as i32).min(0) as f32;
|
||||||
Some(fsrs::Card {
|
Some(fsrs::Card {
|
||||||
id: card.id.0,
|
id: card.id.0,
|
||||||
difficulty: state.difficulty,
|
difficulty: memory_state.difficulty,
|
||||||
stability: state.stability,
|
stability: memory_state.stability,
|
||||||
last_date,
|
last_date,
|
||||||
due: relative_due as f32,
|
due: relative_due as f32,
|
||||||
interval: card.interval as f32,
|
interval: card.interval as f32,
|
||||||
|
@ -269,21 +287,17 @@ impl Card {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
CardQueue::New => None,
|
CardQueue::New => None,
|
||||||
CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => {
|
CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => Some(fsrs::Card {
|
||||||
Some(fsrs::Card {
|
|
||||||
id: card.id.0,
|
id: card.id.0,
|
||||||
difficulty: state.difficulty,
|
difficulty: memory_state.difficulty,
|
||||||
stability: state.stability,
|
stability: memory_state.stability,
|
||||||
last_date: 0.0,
|
last_date: 0.0,
|
||||||
due: 0.0,
|
due: 0.0,
|
||||||
interval: card.interval as f32,
|
interval: card.interval as f32,
|
||||||
lapses: card.lapses,
|
lapses: card.lapses,
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
CardQueue::PreviewRepeat => None,
|
CardQueue::PreviewRepeat => None,
|
||||||
CardQueue::Suspended => None,
|
CardQueue::Suspended => None,
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
import { isApplePlatform } from "@tslib/platform";
|
import { chromiumVersion, isApplePlatform } from "@tslib/platform";
|
||||||
import { getPlatformString } from "@tslib/shortcuts";
|
import { getPlatformString } from "@tslib/shortcuts";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
@ -22,9 +22,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const { focusedInput, fields } = noteEditorContext.get();
|
const { focusedInput, fields } = noteEditorContext.get();
|
||||||
|
|
||||||
// Workaround for Cmd+Option+Shift+C not working on macOS. The keyup approach works
|
// Workaround for Cmd+Option+Shift+C not working on macOS on older Chromium
|
||||||
// on Linux as well, but fails on Windows.
|
// versions.
|
||||||
const event = isApplePlatform() ? "keyup" : "keydown";
|
const chromiumVer = chromiumVersion();
|
||||||
|
const event =
|
||||||
|
isApplePlatform() && chromiumVer != null && chromiumVer <= 112
|
||||||
|
? "keyup"
|
||||||
|
: "keydown";
|
||||||
|
|
||||||
const clozePattern = /\{\{c(\d+)::/gu;
|
const clozePattern = /\{\{c(\d+)::/gu;
|
||||||
function getCurrentHighestCloze(increment: boolean): number {
|
function getCurrentHighestCloze(increment: boolean): number {
|
||||||
|
|
|
@ -13,3 +13,20 @@ export function isApplePlatform(): boolean {
|
||||||
export function isDesktop(): boolean {
|
export function isDesktop(): boolean {
|
||||||
return !(/iphone|ipad|ipod|android/i.test(window.navigator.userAgent));
|
return !(/iphone|ipad|ipod|android/i.test(window.navigator.userAgent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function chromiumVersion(): number | null {
|
||||||
|
const userAgent = window.navigator.userAgent;
|
||||||
|
|
||||||
|
// Check if it's a Chromium-based browser (Chrome, Edge, Opera, etc.)
|
||||||
|
// but exclude Safari which also contains "Chrome" in its user agent
|
||||||
|
if (userAgent.includes("Safari") && !userAgent.includes("Chrome")) {
|
||||||
|
return null; // Safari
|
||||||
|
}
|
||||||
|
|
||||||
|
const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
|
||||||
|
if (chromeMatch) {
|
||||||
|
return parseInt(chromeMatch[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Not a Chromium-based browser
|
||||||
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
.easy-days-settings input[type="range"] {
|
.easy-days-settings input[type="range"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day {
|
.day {
|
||||||
|
|
|
@ -95,6 +95,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit,
|
newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit,
|
||||||
easyDaysPercentages: $config.easyDaysPercentages,
|
easyDaysPercentages: $config.easyDaysPercentages,
|
||||||
reviewOrder: $config.reviewOrder,
|
reviewOrder: $config.reviewOrder,
|
||||||
|
historicalRetention: $config.historicalRetention,
|
||||||
});
|
});
|
||||||
|
|
||||||
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
|
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
|
||||||
|
|
|
@ -47,6 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
font-family: inherit;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n";
|
||||||
import { optimumPixelSizeForCanvas } from "./canvas-scale";
|
import { optimumPixelSizeForCanvas } from "./canvas-scale";
|
||||||
import { Shape } from "./shapes";
|
import { Shape } from "./shapes";
|
||||||
import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } from "./shapes";
|
import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } from "./shapes";
|
||||||
import { TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib";
|
import { SHAPE_MASK_COLOR, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib";
|
||||||
import type { Size } from "./types";
|
import type { Size } from "./types";
|
||||||
|
|
||||||
export type DrawShapesData = {
|
export type DrawShapesData = {
|
||||||
|
@ -217,7 +217,7 @@ function drawShapes(
|
||||||
context,
|
context,
|
||||||
size,
|
size,
|
||||||
shape,
|
shape,
|
||||||
fill: shape.fill ?? properties.inActiveShapeColor,
|
fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor,
|
||||||
stroke: properties.inActiveBorder.color,
|
stroke: properties.inActiveBorder.color,
|
||||||
strokeWidth: properties.inActiveBorder.width,
|
strokeWidth: properties.inActiveBorder.width,
|
||||||
});
|
});
|
||||||
|
@ -437,7 +437,7 @@ function getShapeProperties(): ShapeProperties {
|
||||||
activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e",
|
activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e",
|
||||||
inActiveShapeColor: inActiveShapeColor
|
inActiveShapeColor: inActiveShapeColor
|
||||||
? inActiveShapeColor
|
? inActiveShapeColor
|
||||||
: "#ffeba2",
|
: SHAPE_MASK_COLOR,
|
||||||
highlightShapeColor: highlightShapeColor
|
highlightShapeColor: highlightShapeColor
|
||||||
? highlightShapeColor
|
? highlightShapeColor
|
||||||
: "#ff8e8e00",
|
: "#ff8e8e00",
|
||||||
|
|
48
uv.lock
48
uv.lock
|
@ -66,7 +66,7 @@ requires-dist = [
|
||||||
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
|
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
{ name = "protobuf", specifier = ">=4.21" },
|
{ name = "protobuf", specifier = ">=6.0,<8.0" },
|
||||||
{ name = "requests", extras = ["socks"] },
|
{ name = "requests", extras = ["socks"] },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
|
@ -170,8 +170,8 @@ dependencies = [
|
||||||
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-webengine", version = "6.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6-webengine", version = "6.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or extra == 'extra-3-aqt-qt68' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" },
|
||||||
{ name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "send2trash" },
|
{ name = "send2trash" },
|
||||||
|
@ -186,8 +186,8 @@ qt = [
|
||||||
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-webengine-qt6", version = "6.8.2", source = { registry = "https://pypi.org/simple" } },
|
||||||
]
|
]
|
||||||
qt66 = [
|
qt66 = [
|
||||||
{ name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" } },
|
||||||
|
@ -234,11 +234,11 @@ requires-dist = [
|
||||||
{ name = "pyqt6-sip", marker = "extra == 'qt67'", specifier = "==13.10.2" },
|
{ name = "pyqt6-sip", marker = "extra == 'qt67'", specifier = "==13.10.2" },
|
||||||
{ name = "pyqt6-sip", marker = "extra == 'qt68'", specifier = "==13.10.2" },
|
{ name = "pyqt6-sip", marker = "extra == 'qt68'", specifier = "==13.10.2" },
|
||||||
{ name = "pyqt6-webengine", specifier = ">=6.2" },
|
{ name = "pyqt6-webengine", specifier = ">=6.2" },
|
||||||
{ name = "pyqt6-webengine", marker = "extra == 'qt'", specifier = "==6.9.0" },
|
{ name = "pyqt6-webengine", marker = "extra == 'qt'", specifier = "==6.8.0" },
|
||||||
{ name = "pyqt6-webengine", marker = "extra == 'qt66'", specifier = "==6.6.0" },
|
{ name = "pyqt6-webengine", marker = "extra == 'qt66'", specifier = "==6.6.0" },
|
||||||
{ name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" },
|
{ name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" },
|
||||||
{ name = "pyqt6-webengine", marker = "extra == 'qt68'", specifier = "==6.8.0" },
|
{ name = "pyqt6-webengine", marker = "extra == 'qt68'", specifier = "==6.8.0" },
|
||||||
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt'", specifier = "==6.9.1" },
|
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt'", specifier = "==6.8.2" },
|
||||||
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt66'", specifier = "==6.6.2" },
|
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt66'", specifier = "==6.6.2" },
|
||||||
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" },
|
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" },
|
||||||
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt68'", specifier = "==6.8.1" },
|
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt68'", specifier = "==6.8.1" },
|
||||||
|
@ -535,7 +535,7 @@ name = "importlib-metadata"
|
||||||
version = "8.7.0"
|
version = "8.7.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "zipp" },
|
{ name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
@ -1017,8 +1017,8 @@ resolution-markers = [
|
||||||
"python_full_version < '3.10'",
|
"python_full_version < '3.10'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
@ -1229,8 +1229,10 @@ resolution-markers = [
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
|
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or extra == 'extra-3-aqt-qt68' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" },
|
||||||
{ name = "pyqt6-webengine-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
{ name = "pyqt6-webengine-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" },
|
||||||
|
{ name = "pyqt6-webengine-qt6", version = "6.8.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/c8/cadaa950eaf97f29e48c435e274ea5a81c051e745a3e2f5d9d994b7a6cda/PyQt6_WebEngine-6.8.0.tar.gz", hash = "sha256:64045ea622b6a41882c2b18f55ae9714b8660acff06a54e910eb72822c2f3ff2", size = 34203, upload-time = "2024-12-12T15:34:35.573Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cd/c8/cadaa950eaf97f29e48c435e274ea5a81c051e745a3e2f5d9d994b7a6cda/PyQt6_WebEngine-6.8.0.tar.gz", hash = "sha256:64045ea622b6a41882c2b18f55ae9714b8660acff06a54e910eb72822c2f3ff2", size = 34203, upload-time = "2024-12-12T15:34:35.573Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
@ -1252,9 +1254,9 @@ resolution-markers = [
|
||||||
"python_full_version < '3.10'",
|
"python_full_version < '3.10'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
{ name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } },
|
{ name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/1a/9971af004a7e859347702f816fb71ecd67c3e32b2f0ae8daf1c1ded99f62/pyqt6_webengine-6.9.0.tar.gz", hash = "sha256:6ae537e3bbda06b8e06535e4852297e0bc3b00543c47929541fcc9b11981aa25", size = 34616, upload-time = "2025-04-08T08:57:35.402Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8f/1a/9971af004a7e859347702f816fb71ecd67c3e32b2f0ae8daf1c1ded99f62/pyqt6_webengine-6.9.0.tar.gz", hash = "sha256:6ae537e3bbda06b8e06535e4852297e0bc3b00543c47929541fcc9b11981aa25", size = 34616, upload-time = "2025-04-08T08:57:35.402Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
@ -1321,6 +1323,24 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/b5/a641ebe3e5113bee23d911c58fdd2e65061a6e3786a26b068468b988e5d2/PyQt6_WebEngine_Qt6-6.8.1-py3-none-win_amd64.whl", hash = "sha256:0ced2a10433da2571cfa29ed882698e0e164184d54068d17ba73799c45af5f0f", size = 95657750, upload-time = "2024-12-06T13:47:43.048Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/b5/a641ebe3e5113bee23d911c58fdd2e65061a6e3786a26b068468b988e5d2/PyQt6_WebEngine_Qt6-6.8.1-py3-none-win_amd64.whl", hash = "sha256:0ced2a10433da2571cfa29ed882698e0e164184d54068d17ba73799c45af5f0f", size = 95657750, upload-time = "2024-12-06T13:47:43.048Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyqt6-webengine-qt6"
|
||||||
|
version = "6.8.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12'",
|
||||||
|
"python_full_version == '3.11.*'",
|
||||||
|
"python_full_version == '3.10.*'",
|
||||||
|
"python_full_version < '3.10'",
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/da/639523b821d68a253f7fb2a8a4f2b277f5a03e9adba5a9cfcc2aa1aa9ed1/PyQt6_WebEngine_Qt6-6.8.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:84312705615b5fccedb386531bbd505eb110469444d778f09acd6a214836789e", size = 113127300, upload-time = "2025-02-06T12:05:55.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/bd/33b89cc7cdf54d172be3f98746273b4b6fba73b4802a2e5a6fa757951b47/PyQt6_WebEngine_Qt6-6.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:672363b3809973bbe3408048fc49e98f5c54db8629e855d813fd531e05929007", size = 101984083, upload-time = "2025-02-06T12:06:09.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/90/2693e9de1f064ac7cc10ba25548bbab6ce45a163eef07a22db3ff5ce8b81/PyQt6_WebEngine_Qt6-6.8.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c3be75ef7563b965306de53cae0b357438672d3bf7d9b39edacc307fbeb9965e", size = 105210886, upload-time = "2025-02-06T12:06:23.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/8a/f30075726c8ac391b6fbbc7ab043795ec79f56e452e9d835b883576738b2/PyQt6_WebEngine_Qt6-6.8.2-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:72c1b4c45a3226f32f6c821ee474c4418727913536a62506d9787e24a46d6f27", size = 101194628, upload-time = "2025-02-06T12:06:37.196Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/2a/4fe2bfd3a1ed0e27d1b8f32a5259ebe966432365391c9a541f290f5438de/PyQt6_WebEngine_Qt6-6.8.2-py3-none-win_amd64.whl", hash = "sha256:4421159f3ac4a796499b7f73e98028797a4ae636b04f920b8165308ca0b8c629", size = 95573175, upload-time = "2025-02-06T12:06:49.642Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqt6-webengine-qt6"
|
name = "pyqt6-webengine-qt6"
|
||||||
version = "6.9.1"
|
version = "6.9.1"
|
||||||
|
|
Loading…
Reference in a new issue