Merge branch 'main' into rust

This commit is contained in:
Abdo 2025-12-31 06:37:04 +03:00 committed by GitHub
commit f873e68e31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 75 additions and 64 deletions

View file

@ -255,6 +255,7 @@ Ranjit Odedra <ranjitodedra.dev@gmail.com>
Eltaurus <https://github.com/Eltaurus-Lt> Eltaurus <https://github.com/Eltaurus-Lt>
jariji jariji
Francisco Esteva <fr.esteva@duocuc.cl> Francisco Esteva <fr.esteva@duocuc.cl>
SelfishPig <https://github.com/SelfishPig>
******************** ********************

View file

@ -234,7 +234,7 @@ class DeckBrowser:
if node.collapsed: if node.collapsed:
prefix = "+" prefix = "+"
else: else:
prefix = "-" prefix = ""
def indent() -> str: def indent() -> str:
return "&nbsp;" * 6 * (node.level - 1) return "&nbsp;" * 6 * (node.level - 1)

View file

@ -14,7 +14,7 @@ from markdown import markdown
import aqt import aqt
from anki.collection import HelpPage from anki.collection import HelpPage
from anki.errors import BackendError, Interrupted from anki.errors import BackendError, CardTypeError, Interrupted
from anki.utils import is_win from anki.utils import is_win
from aqt.addons import AddonManager, AddonMeta from aqt.addons import AddonManager, AddonMeta
from aqt.qt import * from aqt.qt import *
@ -36,6 +36,14 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None:
global _mbox global _mbox
error_lines = [] error_lines = []
help_page = HelpPage.TROUBLESHOOTING help_page = HelpPage.TROUBLESHOOTING
# default to PlainText
text_format = Qt.TextFormat.PlainText
# set CardTypeError messages as rich text to allow HTML formatting
if isinstance(exception, CardTypeError):
text_format = Qt.TextFormat.RichText
if isinstance(exception, BackendError): if isinstance(exception, BackendError):
if exception.context: if exception.context:
error_lines.append(exception.context) error_lines.append(exception.context)
@ -51,7 +59,7 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None:
) )
error_text = "\n".join(error_lines) error_text = "\n".join(error_lines)
print(error_lines) print(error_lines)
_mbox = _init_message_box(str(exception), error_text, help_page) _mbox = _init_message_box(str(exception), error_text, help_page, text_format)
_mbox.show() _mbox.show()
@ -171,7 +179,10 @@ if not os.environ.get("DEBUG"):
def _init_message_box( def _init_message_box(
user_text: str, debug_text: str, help_page=HelpPage.TROUBLESHOOTING user_text: str,
debug_text: str,
help_page=HelpPage.TROUBLESHOOTING,
text_format=Qt.TextFormat.PlainText,
): ):
global _mbox global _mbox
@ -179,7 +190,7 @@ def _init_message_box(
_mbox.setWindowTitle("Anki") _mbox.setWindowTitle("Anki")
_mbox.setText(user_text) _mbox.setText(user_text)
_mbox.setIcon(QMessageBox.Icon.Warning) _mbox.setIcon(QMessageBox.Icon.Warning)
_mbox.setTextFormat(Qt.TextFormat.PlainText) _mbox.setTextFormat(text_format)
def show_help(): def show_help():
openHelp(help_page) openHelp(help_page)

View file

@ -8,7 +8,6 @@ mod python;
mod typescript; mod typescript;
mod write_strings; mod write_strings;
use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use anki_io::create_dir_all; use anki_io::create_dir_all;
@ -32,8 +31,7 @@ fn main() -> Result<()> {
python::write_py_interface(&modules)?; python::write_py_interface(&modules)?;
// write strings.json file to requested path // write strings.json file to requested path
println!("cargo:rerun-if-env-changed=STRINGS_JSON"); if let Some(path) = option_env!("STRINGS_JSON") {
if let Ok(path) = env::var("STRINGS_JSON") {
if !path.is_empty() { if !path.is_empty() {
let path = PathBuf::from(path); let path = PathBuf::from(path);
let meta_json = serde_json::to_string_pretty(&modules).unwrap(); let meta_json = serde_json::to_string_pretty(&modules).unwrap();

View file

@ -1,7 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::env;
use std::fmt::Write; use std::fmt::Write;
use std::path::PathBuf; use std::path::PathBuf;
@ -21,7 +20,7 @@ pub fn write_py_interface(modules: &[Module]) -> Result<()> {
render_methods(modules, &mut out); render_methods(modules, &mut out);
render_legacy_enum(modules, &mut out); render_legacy_enum(modules, &mut out);
if let Ok(path) = env::var("STRINGS_PY") { if let Some(path) = option_env!("STRINGS_PY") {
let path = PathBuf::from(path); let path = PathBuf::from(path);
create_dir_all(path.parent().unwrap())?; create_dir_all(path.parent().unwrap())?;
write_file_if_changed(path, out)?; write_file_if_changed(path, out)?;

View file

@ -1,7 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::env;
use std::fmt::Write; use std::fmt::Write;
use std::path::PathBuf; use std::path::PathBuf;
@ -22,7 +21,7 @@ pub fn write_ts_interface(modules: &[Module]) -> Result<()> {
render_module_map(modules, &mut ts_out); render_module_map(modules, &mut ts_out);
render_methods(modules, &mut ts_out); render_methods(modules, &mut ts_out);
if let Ok(path) = env::var("STRINGS_TS") { if let Some(path) = option_env!("STRINGS_TS") {
let path = PathBuf::from(path); let path = PathBuf::from(path);
create_dir_all(path.parent().unwrap())?; create_dir_all(path.parent().unwrap())?;
write_file_if_changed(path, ts_out)?; write_file_if_changed(path, ts_out)?;

View file

@ -335,6 +335,15 @@ pub fn write_file_if_changed(path: impl AsRef<Path>, contents: impl AsRef<[u8]>)
.map(|existing| existing != contents) .map(|existing| existing != contents)
.unwrap_or(true) .unwrap_or(true)
}; };
match std::env::var("CARGO_PKG_NAME") {
Ok(pkg) if pkg == "anki_proto" || pkg == "anki_i18n" => {
// at comptime for the proto/i18n crates, register implicit output as input
println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
}
_ => {}
}
if changed { if changed {
write_file(path, contents)?; write_file(path, contents)?;
Ok(true) Ok(true)

View file

@ -434,9 +434,10 @@ impl SqlWriter<'_> {
let timing = self.col.timing_today()?; let timing = self.col.timing_today()?;
(timing.days_elapsed, timing.next_day_at, timing.now) (timing.days_elapsed, timing.next_day_at, timing.now)
}; };
const NEW_TYPE: i8 = CardType::New as i8;
write!( write!(
self.sql, self.sql,
"extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}, {now}) {op} {r}" "case when c.type = {NEW_TYPE} then false else (extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}, {now}) {op} {r}) end"
) )
.unwrap() .unwrap()
} }

View file

@ -54,7 +54,7 @@ fn build_retrievability_query(
) -> String { ) -> String {
if fsrs { if fsrs {
format!( format!(
"extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, {today}, ivl, {next_day_at}, {now}) {order}" "extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, ivl, {today}, {next_day_at}, {now}) {order}"
) )
} else { } else {
format!( format!(

View file

@ -837,7 +837,7 @@ impl fmt::Display for ReviewOrderSubclause {
let next_day_at = timing.next_day_at.0; let next_day_at = timing.next_day_at.0;
let now = timing.now.0; let now = timing.now.0;
temp_string = temp_string =
format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, {today}, ivl, {next_day_at}, {now}) {order}"); format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, ivl, {today}, {next_day_at}, {now}) {order}");
&temp_string &temp_string
} }
ReviewOrderSubclause::Added => "nid asc, ord asc", ReviewOrderSubclause::Added => "nid asc, ord asc",

View file

@ -332,23 +332,30 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
return Ok(None); return Ok(None);
}; };
let seconds_elapsed = if let Some(last_review_time) = card_data.last_review_time { let seconds_elapsed = if let Some(last_review_time) = card_data.last_review_time {
now.saturating_sub(last_review_time.0) as u32 // This and any following
// (x as u32).saturating_sub(y as u32)
// must not be changed to
// x.saturating_sub(y) as u32
// as x and y are i64's and saturating_sub will therfore allow negative numbers
// before converting to u32 in the latter example.
(now as u32).saturating_sub(last_review_time.0 as u32)
} else if due > 365_000 { } else if due > 365_000 {
// (re)learning card in seconds // (re)learning card in seconds
let Ok(ivl) = ctx.get_raw(2).as_i64() else { let Ok(ivl) = ctx.get_raw(2).as_i64() else {
return Ok(None); return Ok(None);
}; };
let last_review_time = due.saturating_sub(ivl); let last_review_time = (due as u32).saturating_sub(ivl as u32);
now.saturating_sub(last_review_time) as u32 (now as u32).saturating_sub(last_review_time)
} else { } else {
let Ok(ivl) = ctx.get_raw(2).as_i64() else { let Ok(ivl) = ctx.get_raw(2).as_i64() else {
return Ok(None); return Ok(None);
}; };
let Ok(days_elapsed) = ctx.get_raw(3).as_i64() else { // timing.days_elapsed
let Ok(today) = ctx.get_raw(3).as_i64() else {
return Ok(None); return Ok(None);
}; };
let review_day = due.saturating_sub(ivl); let review_day = (due as u32).saturating_sub(ivl as u32);
days_elapsed.saturating_sub(review_day) as u32 * 86_400 (today as u32).saturating_sub(review_day) * 86_400
}; };
let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY); let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY);
let retrievability = card_data.memory_state().map(|state| { let retrievability = card_data.memory_state().map(|state| {
@ -364,7 +371,7 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> {
} }
/// eg. extract_fsrs_relative_retrievability(card.data, card.due, /// eg. extract_fsrs_relative_retrievability(card.data, card.due,
/// timing.days_elapsed, card.ivl, timing.next_day_at, timing.now) -> float | /// card.ivl, timing.days_elapsed, timing.next_day_at, timing.now) -> float |
/// null. The higher the number, the higher the card's retrievability relative /// null. The higher the number, the higher the card's retrievability relative
/// to the configured desired retention. /// to the configured desired retention.
fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> { fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> {
@ -378,25 +385,32 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
let Ok(due) = ctx.get_raw(1).as_i64() else { let Ok(due) = ctx.get_raw(1).as_i64() else {
return Ok(None); return Ok(None);
}; };
let Ok(interval) = ctx.get_raw(3).as_i64() else { let Ok(interval) = ctx.get_raw(2).as_i64() else {
return Ok(None); return Ok(None);
}; };
/*
// Unused
let Ok(next_day_at) = ctx.get_raw(4).as_i64() else { let Ok(next_day_at) = ctx.get_raw(4).as_i64() else {
return Ok(None); return Ok(None);
}; };
*/
let Ok(now) = ctx.get_raw(5).as_i64() else { let Ok(now) = ctx.get_raw(5).as_i64() else {
return Ok(None); return Ok(None);
}; };
let days_elapsed = if due > 365_000 { let secs_elapsed = if due > 365_000 {
// (re)learning // (re)learning card with due in seconds
(next_day_at as u32).saturating_sub(due as u32) / 86_400
// Don't change this to now.subtracting_sub(due) as u32
// for the same reasons listed in the comment
// in add_extract_fsrs_retrievability
(now as u32).saturating_sub(due as u32)
} else { } else {
let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { // timing.days_elapsed
let Ok(today) = ctx.get_raw(3).as_i64() else {
return Ok(None); return Ok(None);
}; };
let review_day = due.saturating_sub(interval); let review_day = due.saturating_sub(interval);
(today as u32).saturating_sub(review_day as u32) * 86_400
(days_elapsed as u32).saturating_sub(review_day as u32)
}; };
if let Ok(card_data) = ctx.get_raw(0).as_str() { if let Ok(card_data) = ctx.get_raw(0).as_str() {
if !card_data.is_empty() { if !card_data.is_empty() {
@ -410,23 +424,12 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
let seconds_elapsed = let seconds_elapsed =
if let Some(last_review_time) = card_data.last_review_time { if let Some(last_review_time) = card_data.last_review_time {
now.saturating_sub(last_review_time.0) as u32 // Don't change this to now.subtracting_sub(due) as u32
} else if due > 365_000 { // for the same reasons listed in the comment
// (re)learning card in seconds // in add_extract_fsrs_retrievability
let Ok(ivl) = ctx.get_raw(2).as_i64() else { (now as u32).saturating_sub(last_review_time.0 as u32)
return Ok(None);
};
let last_review_time = due.saturating_sub(ivl);
now.saturating_sub(last_review_time) as u32
} else { } else {
let Ok(ivl) = ctx.get_raw(2).as_i64() else { secs_elapsed
return Ok(None);
};
let Ok(days_elapsed) = ctx.get_raw(3).as_i64() else {
return Ok(None);
};
let review_day = due.saturating_sub(ivl);
days_elapsed.saturating_sub(review_day) as u32 * 86_400
}; };
let current_retrievability = FSRS::new(None) let current_retrievability = FSRS::new(None)
@ -441,7 +444,7 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result
} }
} }
} }
let days_elapsed = secs_elapsed / 86_400;
// FSRS data missing; fall back to SM2 ordering // FSRS data missing; fall back to SM2 ordering
Ok(Some( Ok(Some(
-((days_elapsed as f32) + 0.001) / (interval as f32).max(1.0), -((days_elapsed as f32) + 0.001) / (interval as f32).max(1.0),

View file

@ -202,7 +202,7 @@ fn sveltekit_temp_file(path: &str) -> bool {
} }
fn check_cargo_deny() -> Result<()> { fn check_cargo_deny() -> Result<()> {
Command::run("cargo install cargo-deny@0.18.3")?; Command::run("cargo install cargo-deny@0.18.6")?;
Command::run("cargo deny check")?; Command::run("cargo deny check")?;
Ok(()) Ok(())
} }

View file

@ -10,9 +10,6 @@ export function allImagesLoaded(): Promise<void[]> {
} }
function imageLoaded(img: HTMLImageElement): Promise<void> { function imageLoaded(img: HTMLImageElement): Promise<void> {
if (!img.getAttribute("decoding")) {
img.decoding = "async";
}
return img.complete return img.complete
? Promise.resolve() ? Promise.resolve()
: new Promise((resolve) => { : new Promise((resolve) => {
@ -31,6 +28,8 @@ function extractImageSrcs(fragment: DocumentFragment): string[] {
function createImage(src: string): HTMLImageElement { function createImage(src: string): HTMLImageElement {
const img = new Image(); const img = new Image();
img.src = src; img.src = src;
img.decoding = "async";
img.decode();
return img; return img;
} }

View file

@ -37,7 +37,9 @@ export const addOrUpdateNote = async function(
backExtra, backExtra,
tags, tags,
}); });
showResult(mode.noteId, result, noteCount); if (result.note) {
showResult(mode.noteId, result, noteCount);
}
} else { } else {
const result = await addImageOcclusionNote({ const result = await addImageOcclusionNote({
// IOCloningMode is not used on mobile // IOCloningMode is not used on mobile
@ -55,23 +57,12 @@ export const addOrUpdateNote = async function(
// show toast message // show toast message
const showResult = (noteId: number | null, result: OpChanges, count: number) => { const showResult = (noteId: number | null, result: OpChanges, count: number) => {
const props = $state({ const props = $state({
message: "", message: noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count }),
type: "error" as "error" | "success", type: "success" as "error" | "success",
showToast: true, showToast: true,
}); });
mount(Toast, { mount(Toast, {
target: document.body, target: document.body,
props, props,
}); });
if (result.note) {
const msg = noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count });
props.message = msg;
props.type = "success";
props.showToast = true;
} else {
const msg = tr.notetypesErrorGeneratingCloze();
props.message = msg;
props.showToast = true;
}
}; };