Merge branch 'main' into editor-3830

This commit is contained in:
Abdo 2025-07-27 01:41:08 +03:00
commit 543f97eb10
15 changed files with 1606 additions and 1324 deletions

View file

@ -1 +1 @@
25.07.3 25.07.5

@ -1 +1 @@
Subproject commit b90ef6f03c251eb336029ac7c5f551200d41273f Subproject commit 939298f7c461407951988f362b1a08b451336a1e

@ -1 +1 @@
Subproject commit 9aa63c335c61b30421d39cf43fd8e3975179059c Subproject commit bc2da83c77749d96f3df8144f00c87d68dd2187a

View file

@ -133,6 +133,7 @@ class Card(DeprecatedNamesMixin):
memory_state=self.memory_state, memory_state=self.memory_state,
desired_retention=self.desired_retention, desired_retention=self.desired_retention,
decay=self.decay, decay=self.decay,
last_review_time_secs=self.last_review_time,
) )
@deprecated(info="please use col.update_card()") @deprecated(info="please use col.update_card()")

View file

@ -51,6 +51,7 @@ class CardInfoDialog(QDialog):
def _setup_ui(self, card_id: CardId | None) -> None: def _setup_ui(self, card_id: CardId | None) -> None:
self.mw.garbage_collect_on_dialog_finish(self) self.mw.garbage_collect_on_dialog_finish(self)
self.setMinimumSize(400, 300)
disable_help_button(self) disable_help_button(self)
restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800)) restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800))
add_close_shortcut(self) add_close_shortcut(self)

View file

@ -633,7 +633,7 @@ class QtAudioInputRecorder(Recorder):
from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore
format = QAudioFormat() format = QAudioFormat()
format.setChannelCount(1) format.setChannelCount(2)
format.setSampleRate(44100) format.setSampleRate(44100)
format.setSampleFormat(QAudioFormat.SampleFormat.Int16) format.setSampleFormat(QAudioFormat.SampleFormat.Int16)

View file

@ -38,19 +38,19 @@ cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/" cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
cp ../versions.py "$APP_LAUNCHER/Contents/Resources/" cp ../versions.py "$APP_LAUNCHER/Contents/Resources/"
# Codesign # Codesign/bundle
for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do
codesign --force -vvvv -o runtime -s "Developer ID Application:" \
--entitlements entitlements.python.xml \
"$i"
done
# Check
codesign -vvv "$APP_LAUNCHER"
spctl -a "$APP_LAUNCHER"
# Notarize and bundle (skip if NODMG is set)
if [ -z "$NODMG" ]; then if [ -z "$NODMG" ]; then
for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do
codesign --force -vvvv -o runtime -s "Developer ID Application:" \
--entitlements entitlements.python.xml \
"$i"
done
# Check
codesign -vvv "$APP_LAUNCHER"
spctl -a "$APP_LAUNCHER"
# Notarize and build dmg
./notarize.sh "$OUTPUT_DIR" ./notarize.sh "$OUTPUT_DIR"
./dmg/build.sh "$OUTPUT_DIR" ./dmg/build.sh "$OUTPUT_DIR"
fi fi

View file

@ -11,7 +11,6 @@ use std::time::SystemTime;
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use anki_io::copy_file; use anki_io::copy_file;
use anki_io::copy_if_newer;
use anki_io::create_dir_all; use anki_io::create_dir_all;
use anki_io::modified_time; use anki_io::modified_time;
use anki_io::read_file; use anki_io::read_file;
@ -50,6 +49,7 @@ struct State {
pyproject_modified_by_user: bool, pyproject_modified_by_user: bool,
previous_version: Option<String>, previous_version: Option<String>,
resources_dir: std::path::PathBuf, resources_dir: std::path::PathBuf,
venv_folder: std::path::PathBuf,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -111,6 +111,7 @@ fn run() -> Result<()> {
pyproject_modified_by_user: false, // calculated later pyproject_modified_by_user: false, // calculated later
previous_version: None, previous_version: None,
resources_dir, resources_dir,
venv_folder: uv_install_root.join(".venv"),
}; };
// Check for uninstall request from Windows uninstaller // Check for uninstall request from Windows uninstaller
@ -120,15 +121,11 @@ fn run() -> Result<()> {
return Ok(()); return Ok(());
} }
// Create install directory and copy project files in // Create install directory
create_dir_all(&state.uv_install_root)?; create_dir_all(&state.uv_install_root)?;
copy_if_newer(&state.dist_pyproject_path, &state.user_pyproject_path)?;
copy_if_newer(
&state.dist_python_version_path,
&state.user_python_version_path,
)?;
let launcher_requested = state.launcher_trigger_file.exists(); let launcher_requested =
state.launcher_trigger_file.exists() || !state.user_pyproject_path.exists();
// Calculate whether user has custom edits that need syncing // Calculate whether user has custom edits that need syncing
let pyproject_time = file_timestamp_secs(&state.user_pyproject_path); let pyproject_time = file_timestamp_secs(&state.user_pyproject_path);
@ -158,7 +155,7 @@ fn run() -> Result<()> {
check_versions(&mut state); check_versions(&mut state);
let first_run = !state.uv_install_root.join(".venv").exists(); let first_run = !state.venv_folder.exists();
if first_run { if first_run {
handle_version_install_or_update(&state, MainMenuChoice::Latest)?; handle_version_install_or_update(&state, MainMenuChoice::Latest)?;
} else { } else {
@ -166,7 +163,7 @@ fn run() -> Result<()> {
} }
// Write marker file to indicate we've completed the sync process // Write marker file to indicate we've completed the sync process
write_sync_marker(&state.sync_complete_marker)?; write_sync_marker(&state)?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@ -192,13 +189,15 @@ fn run() -> Result<()> {
Ok(()) Ok(())
} }
fn extract_aqt_version( fn extract_aqt_version(state: &State) -> Option<String> {
uv_path: &std::path::Path, // Check if .venv exists first
uv_install_root: &std::path::Path, if !state.venv_folder.exists() {
) -> Option<String> { return None;
let output = Command::new(uv_path) }
.current_dir(uv_install_root)
.env("VIRTUAL_ENV", uv_install_root.join(".venv")) let output = Command::new(&state.uv_path)
.current_dir(&state.uv_install_root)
.env("VIRTUAL_ENV", &state.venv_folder)
.args(["pip", "show", "aqt"]) .args(["pip", "show", "aqt"])
.output() .output()
.ok()?; .ok()?;
@ -223,7 +222,7 @@ fn check_versions(state: &mut State) {
} }
// Determine current version by invoking uv pip show aqt // Determine current version by invoking uv pip show aqt
match extract_aqt_version(&state.uv_path, &state.uv_install_root) { match extract_aqt_version(state) {
Some(version) => { Some(version) => {
state.current_version = Some(version); state.current_version = Some(version);
} }
@ -248,12 +247,12 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
update_pyproject_for_version(choice.clone(), state)?; update_pyproject_for_version(choice.clone(), state)?;
// 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);
// Remove sync marker before attempting sync // Remove sync marker before attempting sync
let _ = remove_file(&state.sync_complete_marker); let _ = remove_file(&state.sync_complete_marker);
println!("\x1B[1mUpdating Anki...\x1B[0m\n"); println!("Updating Anki...\n");
let python_version_trimmed = if state.user_python_version_path.exists() { let python_version_trimmed = if state.user_python_version_path.exists() {
let python_version = read_file(&state.user_python_version_path)?; let python_version = read_file(&state.user_python_version_path)?;
@ -264,6 +263,11 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
None None
}; };
let have_venv = state.venv_folder.exists();
if cfg!(target_os = "macos") && !have_developer_tools() && !have_venv {
println!("If you see a pop-up about 'install_name_tool', you can cancel it, and ignore the warning below.\n");
}
// Prepare to sync the venv // Prepare to sync the venv
let mut command = Command::new(&state.uv_path); let mut command = Command::new(&state.uv_path);
command.current_dir(&state.uv_install_root); command.current_dir(&state.uv_install_root);
@ -275,9 +279,27 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
} }
} }
// remove CONDA_PREFIX/bin from PATH to avoid conda interference
#[cfg(target_os = "macos")]
if let Ok(conda_prefix) = std::env::var("CONDA_PREFIX") {
if let Ok(current_path) = std::env::var("PATH") {
let conda_bin = format!("{conda_prefix}/bin");
let filtered_paths: Vec<&str> = current_path
.split(':')
.filter(|&path| path != conda_bin)
.collect();
let new_path = filtered_paths.join(":");
command.env("PATH", new_path);
}
}
command command
.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)
.env(
"UV_HTTP_TIMEOUT",
std::env::var("UV_HTTP_TIMEOUT").unwrap_or_else(|_| "180".to_string()),
)
.args(["sync", "--upgrade", "--managed-python", "--no-config"]); .args(["sync", "--upgrade", "--managed-python", "--no-config"]);
// Add python version if .python-version file exists // Add python version if .python-version file exists
@ -293,7 +315,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
Ok(_) => { Ok(_) => {
// Sync succeeded // Sync succeeded
if matches!(&choice, MainMenuChoice::Version(VersionKind::PyOxidizer(_))) { if matches!(&choice, MainMenuChoice::Version(VersionKind::PyOxidizer(_))) {
inject_helper_addon(&state.uv_install_root)?; inject_helper_addon()?;
} }
// Now that sync succeeded, save the previous version // Now that sync succeeded, save the previous version
@ -364,9 +386,7 @@ fn main_menu_loop(state: &State) -> Result<()> {
continue; continue;
} }
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => { choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
if handle_version_install_or_update(state, choice.clone()).is_err() { handle_version_install_or_update(state, choice.clone())?;
continue;
}
break; break;
} }
} }
@ -374,12 +394,12 @@ fn main_menu_loop(state: &State) -> Result<()> {
Ok(()) Ok(())
} }
fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> { fn write_sync_marker(state: &State) -> Result<()> {
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.context("Failed to get system time")? .context("Failed to get system time")?
.as_secs(); .as_secs();
write_file(sync_complete_marker, timestamp.to_string())?; write_file(&state.sync_complete_marker, timestamp.to_string())?;
Ok(()) Ok(())
} }
@ -473,8 +493,6 @@ 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...");
let releases = get_releases(state)?; let releases = get_releases(state)?;
let releases_str = releases let releases_str = releases
.latest .latest
@ -633,15 +651,32 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
let mut cmd = Command::new(&state.uv_path); let mut cmd = Command::new(&state.uv_path);
cmd.current_dir(&state.uv_install_root) cmd.current_dir(&state.uv_install_root)
.args(["run", "--no-project"]) .args(["run", "--no-project", "--no-config", "--managed-python"])
.arg(&versions_script); .args(["--with", "pip-system-certs"]);
let output = cmd.utf8_output()?; let python_version = read_file(&state.dist_python_version_path)?;
let python_version_str =
String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?;
let version_trimmed = python_version_str.trim();
if !version_trimmed.is_empty() {
cmd.args(["--python", version_trimmed]);
}
cmd.arg(&versions_script);
let output = match cmd.utf8_output() {
Ok(output) => output,
Err(e) => {
print!("Unable to check for Anki versions. Please check your internet connection.\n\n");
return Err(e.into());
}
};
let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?;
Ok(versions) Ok(versions)
} }
fn get_releases(state: &State) -> Result<Releases> { fn get_releases(state: &State) -> Result<Releases> {
println!("Checking for updates...");
let include_prereleases = state.prerelease_marker.exists(); let include_prereleases = state.prerelease_marker.exists();
let all_versions = fetch_versions(state)?; let all_versions = fetch_versions(state)?;
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases); let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
@ -768,7 +803,7 @@ fn parse_version_kind(version: &str) -> Option<VersionKind> {
} }
} }
fn inject_helper_addon(_uv_install_root: &std::path::Path) -> Result<()> { fn inject_helper_addon() -> Result<()> {
let addons21_path = get_anki_addons21_path()?; let addons21_path = get_anki_addons21_path()?;
if !addons21_path.exists() { if !addons21_path.exists() {
@ -870,16 +905,24 @@ fn handle_uninstall(state: &State) -> Result<bool> {
Ok(true) Ok(true)
} }
fn have_developer_tools() -> bool {
Command::new("xcode-select")
.args(["-p"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn build_python_command(state: &State, args: &[String]) -> Result<Command> { fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
let python_exe = if cfg!(target_os = "windows") { let python_exe = if cfg!(target_os = "windows") {
let show_console = std::env::var("ANKI_CONSOLE").is_ok(); let show_console = std::env::var("ANKI_CONSOLE").is_ok();
if show_console { if show_console {
state.uv_install_root.join(".venv/Scripts/python.exe") state.venv_folder.join("Scripts/python.exe")
} else { } else {
state.uv_install_root.join(".venv/Scripts/pythonw.exe") state.venv_folder.join("Scripts/pythonw.exe")
} }
} else { } else {
state.uv_install_root.join(".venv/bin/python") state.venv_folder.join("bin/python")
}; };
let mut cmd = Command::new(&python_exe); let mut cmd = Command::new(&python_exe);

View file

@ -5,6 +5,10 @@ import json
import sys import sys
import urllib.request import urllib.request
import pip_system_certs.wrapt_requests
pip_system_certs.wrapt_requests.inject_truststore()
def main(): def main():
"""Fetch and return all versions from PyPI, sorted by upload time.""" """Fetch and return all versions from PyPI, sorted by upload time."""

View file

@ -11,6 +11,24 @@ use snafu::ensure;
use snafu::ResultExt; use snafu::ResultExt;
use snafu::Snafu; use snafu::Snafu;
#[derive(Debug)]
pub struct CodeDisplay(Option<i32>);
impl std::fmt::Display for CodeDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
Some(code) => write!(f, "{code}"),
None => write!(f, "?"),
}
}
}
impl From<Option<i32>> for CodeDisplay {
fn from(code: Option<i32>) -> Self {
CodeDisplay(code)
}
}
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum Error { pub enum Error {
#[snafu(display("Failed to execute: {cmdline}"))] #[snafu(display("Failed to execute: {cmdline}"))]
@ -18,8 +36,15 @@ pub enum Error {
cmdline: String, cmdline: String,
source: std::io::Error, source: std::io::Error,
}, },
#[snafu(display("Failed with code {code:?}: {cmdline}"))] #[snafu(display("Failed to run ({code}): {cmdline}"))]
ReturnedError { cmdline: String, code: Option<i32> }, ReturnedError { cmdline: String, code: CodeDisplay },
#[snafu(display("Failed to run ({code}): {cmdline}: {stdout}{stderr}"))]
ReturnedWithOutputError {
cmdline: String,
code: CodeDisplay,
stdout: String,
stderr: String,
},
#[snafu(display("Couldn't decode stdout/stderr as utf8"))] #[snafu(display("Couldn't decode stdout/stderr as utf8"))]
InvalidUtf8 { InvalidUtf8 {
cmdline: String, cmdline: String,
@ -71,31 +96,36 @@ impl CommandExt for Command {
status.success(), status.success(),
ReturnedSnafu { ReturnedSnafu {
cmdline: get_cmdline(self), cmdline: get_cmdline(self),
code: status.code(), code: CodeDisplay::from(status.code()),
} }
); );
Ok(self) Ok(self)
} }
fn utf8_output(&mut self) -> Result<Utf8Output> { fn utf8_output(&mut self) -> Result<Utf8Output> {
let cmdline = get_cmdline(self);
let output = self.output().with_context(|_| DidNotExecuteSnafu { let output = self.output().with_context(|_| DidNotExecuteSnafu {
cmdline: get_cmdline(self), cmdline: cmdline.clone(),
})?; })?;
let stdout = String::from_utf8(output.stdout).with_context(|_| InvalidUtf8Snafu {
cmdline: cmdline.clone(),
})?;
let stderr = String::from_utf8(output.stderr).with_context(|_| InvalidUtf8Snafu {
cmdline: cmdline.clone(),
})?;
ensure!( ensure!(
output.status.success(), output.status.success(),
ReturnedSnafu { ReturnedWithOutputSnafu {
cmdline: get_cmdline(self), cmdline,
code: output.status.code(), code: CodeDisplay::from(output.status.code()),
stdout: stdout.clone(),
stderr: stderr.clone(),
} }
); );
Ok(Utf8Output {
stdout: String::from_utf8(output.stdout).with_context(|_| InvalidUtf8Snafu { Ok(Utf8Output { stdout, stderr })
cmdline: get_cmdline(self),
})?,
stderr: String::from_utf8(output.stderr).with_context(|_| InvalidUtf8Snafu {
cmdline: get_cmdline(self),
})?,
})
} }
fn ensure_spawn(&mut self) -> Result<std::process::Child> { fn ensure_spawn(&mut self) -> Result<std::process::Child> {
@ -135,7 +165,10 @@ mod test {
#[cfg(not(windows))] #[cfg(not(windows))]
assert!(matches!( assert!(matches!(
Command::new("false").ensure_success(), Command::new("false").ensure_success(),
Err(Error::ReturnedError { code: Some(1), .. }) Err(Error::ReturnedError {
code: CodeDisplay(_),
..
})
)); ));
} }
} }

View file

@ -12,14 +12,20 @@ impl Collection {
.map(component_to_regex) .map(component_to_regex)
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
let mut tags = vec![]; let mut tags = vec![];
let mut priority = vec![];
self.storage.get_tags_by_predicate(|tag| { self.storage.get_tags_by_predicate(|tag| {
if tags.len() <= limit && filters_match(&filters, tag) { if priority.len() + tags.len() <= limit {
tags.push(tag.to_string()); match filters_match(&filters, tag) {
Some(true) => priority.push(tag.to_string()),
Some(_) => tags.push(tag.to_string()),
_ => {}
}
} }
// we only need the tag name // we only need the tag name
false false
})?; })?;
Ok(tags) priority.append(&mut tags);
Ok(priority)
} }
} }
@ -27,20 +33,26 @@ fn component_to_regex(component: &str) -> Result<Regex> {
Regex::new(&format!("(?i){}", regex::escape(component))).map_err(Into::into) Regex::new(&format!("(?i){}", regex::escape(component))).map_err(Into::into)
} }
fn filters_match(filters: &[Regex], tag: &str) -> bool { /// Returns None if tag wasn't a match, otherwise whether it was a consecutive
/// prefix match
fn filters_match(filters: &[Regex], tag: &str) -> Option<bool> {
let mut remaining_tag_components = tag.split("::"); let mut remaining_tag_components = tag.split("::");
let mut is_prefix = true;
'outer: for filter in filters { 'outer: for filter in filters {
loop { loop {
if let Some(component) = remaining_tag_components.next() { if let Some(component) = remaining_tag_components.next() {
if filter.is_match(component) { if let Some(m) = filter.find(component) {
is_prefix &= m.start() == 0;
continue 'outer; continue 'outer;
} else {
is_prefix = false;
} }
} else { } else {
return false; return None;
} }
} }
} }
true Some(is_prefix)
} }
#[cfg(test)] #[cfg(test)]
@ -50,28 +62,32 @@ mod test {
#[test] #[test]
fn matching() -> Result<()> { fn matching() -> Result<()> {
let filters = &[component_to_regex("b")?]; let filters = &[component_to_regex("b")?];
assert!(filters_match(filters, "ABC")); assert!(filters_match(filters, "ABC").is_some());
assert!(filters_match(filters, "ABC::def")); assert!(filters_match(filters, "ABC::def").is_some());
assert!(filters_match(filters, "def::abc")); assert!(filters_match(filters, "def::abc").is_some());
assert!(!filters_match(filters, "def")); assert!(filters_match(filters, "def").is_none());
let filters = &[component_to_regex("b")?, component_to_regex("E")?]; let filters = &[component_to_regex("b")?, component_to_regex("E")?];
assert!(!filters_match(filters, "ABC")); assert!(filters_match(filters, "ABC").is_none());
assert!(filters_match(filters, "ABC::def")); assert!(filters_match(filters, "ABC::def").is_some());
assert!(!filters_match(filters, "def::abc")); assert!(filters_match(filters, "def::abc").is_none());
assert!(!filters_match(filters, "def")); assert!(filters_match(filters, "def").is_none());
let filters = &[ let filters = &[
component_to_regex("a")?, component_to_regex("a")?,
component_to_regex("c")?, component_to_regex("c")?,
component_to_regex("e")?, component_to_regex("e")?,
]; ];
assert!(!filters_match(filters, "ace")); assert!(filters_match(filters, "ace").is_none());
assert!(!filters_match(filters, "a::c")); assert!(filters_match(filters, "a::c").is_none());
assert!(!filters_match(filters, "c::e")); assert!(filters_match(filters, "c::e").is_none());
assert!(filters_match(filters, "a::c::e")); assert!(filters_match(filters, "a::c::e").is_some());
assert!(filters_match(filters, "a::b::c::d::e")); assert!(filters_match(filters, "a::b::c::d::e").is_some());
assert!(filters_match(filters, "1::a::b::c::d::e::f")); assert!(filters_match(filters, "1::a::b::c::d::e::f").is_some());
assert_eq!(filters_match(filters, "a1::c2::e3"), Some(true));
assert_eq!(filters_match(filters, "a1::c2::?::e4"), Some(false));
assert_eq!(filters_match(filters, "a1::c2::3e"), Some(false));
Ok(()) Ok(())
} }

View file

@ -24,6 +24,7 @@ export const HelpPage = {
displayOrder: "https://docs.ankiweb.net/deck-options.html#display-order", displayOrder: "https://docs.ankiweb.net/deck-options.html#display-order",
maximumReviewsday: "https://docs.ankiweb.net/deck-options.html#maximum-reviewsday", maximumReviewsday: "https://docs.ankiweb.net/deck-options.html#maximum-reviewsday",
newCardsday: "https://docs.ankiweb.net/deck-options.html#new-cardsday", newCardsday: "https://docs.ankiweb.net/deck-options.html#new-cardsday",
limitsFromTop: "https://docs.ankiweb.net/deck-options.html#limits-start-from-top",
dailyLimits: "https://docs.ankiweb.net/deck-options.html#daily-limits", dailyLimits: "https://docs.ankiweb.net/deck-options.html#daily-limits",
audio: "https://docs.ankiweb.net/deck-options.html#audio", audio: "https://docs.ankiweb.net/deck-options.html#audio",
fsrs: "http://docs.ankiweb.net/deck-options.html#fsrs", fsrs: "http://docs.ankiweb.net/deck-options.html#fsrs",

View file

@ -140,7 +140,7 @@
applyAllParentLimits: { applyAllParentLimits: {
title: tr.deckConfigApplyAllParentLimits(), title: tr.deckConfigApplyAllParentLimits(),
help: applyAllParentLimitsHelp, help: applyAllParentLimitsHelp,
url: HelpPage.DeckOptions.newCardsday, url: HelpPage.DeckOptions.limitsFromTop,
global: true, global: true,
}, },
}; };

View file

@ -106,6 +106,9 @@ function initCanvas(): fabric.Canvas {
fabric.Object.prototype.cornerStyle = "circle"; fabric.Object.prototype.cornerStyle = "circle";
fabric.Object.prototype.cornerStrokeColor = "#000000"; fabric.Object.prototype.cornerStrokeColor = "#000000";
fabric.Object.prototype.padding = 8; fabric.Object.prototype.padding = 8;
// snap rotation around 0 by +-3deg
fabric.Object.prototype.snapAngle = 360;
fabric.Object.prototype.snapThreshold = 3;
// disable rotation when selecting // disable rotation when selecting
canvas.on("selection:created", () => { canvas.on("selection:created", () => {
const g = canvas.getActiveObject(); const g = canvas.getActiveObject();

2650
yarn.lock

File diff suppressed because it is too large Load diff