From 5614d20bedcc4dd268136d389ad796b404a69d2c Mon Sep 17 00:00:00 2001 From: llama Date: Thu, 20 Nov 2025 23:43:14 +0800 Subject: [PATCH 1/6] fix(Import): case-fold media filenames when checking uniqueness (#4435) * add wrapper struct with case-folding get impl * use wrapper struct * restrict case-folding to windows * Revert "restrict case-folding to windows" This reverts commit aad01d904f07b28466190d849141883e8a76e1c2. * case-fold filenames for newly added media * add test * fix incorrect comment --- .../package/apkg/import/media.rs | 3 ++- rslib/src/media/files.rs | 12 ++++++++++-- rslib/src/media/mod.rs | 6 +++--- rslib/src/sync/media/database/client/mod.rs | 19 +++++++++++++++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/rslib/src/import_export/package/apkg/import/media.rs b/rslib/src/import_export/package/apkg/import/media.rs index 32bf7c807..20543e074 100644 --- a/rslib/src/import_export/package/apkg/import/media.rs +++ b/rslib/src/import_export/package/apkg/import/media.rs @@ -17,6 +17,7 @@ use crate::import_export::package::media::SafeMediaEntry; use crate::import_export::ImportProgress; use crate::media::files::add_hash_suffix_to_file_stem; use crate::media::files::sha1_of_reader; +use crate::media::Checksums; use crate::prelude::*; use crate::progress::ThrottlingProgressHandler; @@ -75,7 +76,7 @@ impl Context<'_> { fn prepare_media( media_entries: Vec, archive: &mut ZipArchive, - existing_sha1s: &HashMap, + existing_sha1s: &Checksums, progress: &mut ThrottlingProgressHandler, ) -> Result { let mut media_map = MediaUseMap::default(); diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index ce17b40bb..b098eb19e 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -173,7 +173,9 @@ pub fn add_data_to_folder_uniquely<'a, P>( where P: AsRef, { - let normalized_name = normalize_filename(desired_name); + // force lowercase to account for case-insensitive filesystems + // but not within normalize_filename, for existing media refs + let normalized_name: Cow<_> = normalize_filename(desired_name).to_lowercase().into(); let mut target_path = folder.as_ref().join(normalized_name.as_ref()); @@ -496,8 +498,14 @@ mod test { "test.mp3" ); - // different contents + // different contents, filenames differ only by case let h2 = sha1_of_data(b"hello1"); + assert_eq!( + add_data_to_folder_uniquely(dpath, "Test.mp3", b"hello1", h2).unwrap(), + "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" + ); + + // same contents, filenames differ only by case assert_eq!( add_data_to_folder_uniquely(dpath, "test.mp3", b"hello1", h2).unwrap(), "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" diff --git a/rslib/src/media/mod.rs b/rslib/src/media/mod.rs index 259dd52f8..8a599fbec 100644 --- a/rslib/src/media/mod.rs +++ b/rslib/src/media/mod.rs @@ -6,7 +6,6 @@ pub mod files; mod service; use std::borrow::Cow; -use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -22,6 +21,7 @@ use crate::progress::ThrottlingProgressHandler; use crate::sync::http_client::HttpSyncClient; use crate::sync::login::SyncAuth; use crate::sync::media::database::client::changetracker::ChangeTracker; +pub use crate::sync::media::database::client::Checksums; use crate::sync::media::database::client::MediaDatabase; use crate::sync::media::database::client::MediaEntry; use crate::sync::media::progress::MediaSyncProgress; @@ -157,7 +157,7 @@ impl MediaManager { pub fn all_checksums_after_checking( &self, progress: impl FnMut(usize) -> bool, - ) -> Result> { + ) -> Result { ChangeTracker::new(&self.media_folder, progress).register_changes(&self.db)?; self.db.all_registered_checksums() } @@ -176,7 +176,7 @@ impl MediaManager { /// All checksums without registering changes first. #[cfg(test)] - pub(crate) fn all_checksums_as_is(&self) -> HashMap { + pub(crate) fn all_checksums_as_is(&self) -> Checksums { self.db.all_registered_checksums().unwrap() } } diff --git a/rslib/src/sync/media/database/client/mod.rs b/rslib/src/sync/media/database/client/mod.rs index f9c6e5ed1..fe3e7c840 100644 --- a/rslib/src/sync/media/database/client/mod.rs +++ b/rslib/src/sync/media/database/client/mod.rs @@ -18,6 +18,20 @@ use crate::prelude::*; pub mod changetracker; +pub struct Checksums(HashMap); + +impl Checksums { + // case-fold filenames when checking files to be imported + // to account for case-insensitive filesystems + pub fn get(&self, key: impl AsRef) -> Option<&Sha1Hash> { + self.0.get(key.as_ref().to_lowercase().as_str()) + } + + pub fn contains_key(&self, key: impl AsRef) -> bool { + self.get(key).is_some() + } +} + #[derive(Debug, PartialEq, Eq)] pub struct MediaEntry { pub fname: String, @@ -175,11 +189,12 @@ delete from media where fname=?", } /// Returns all filenames and checksums, where the checksum is not null. - pub(crate) fn all_registered_checksums(&self) -> error::Result> { + pub(crate) fn all_registered_checksums(&self) -> error::Result { self.db .prepare("SELECT fname, csum FROM media WHERE csum IS NOT NULL")? .query_and_then([], row_to_name_and_checksum)? - .collect() + .collect::>() + .map(Checksums) } pub(crate) fn force_resync(&self) -> error::Result<()> { From 2d4de33cf3160342c4c704c294e643c3e11071b1 Mon Sep 17 00:00:00 2001 From: Lee Doughty <32392044+leedoughty@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:54:46 +0000 Subject: [PATCH 2/6] Ensure trailing spaces are placed outside cloze deletions (#4446) --- ts/lib/tslib/wrap.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/lib/tslib/wrap.ts b/ts/lib/tslib/wrap.ts index 39b10e9d1..e22c3e6d8 100644 --- a/ts/lib/tslib/wrap.ts +++ b/ts/lib/tslib/wrap.ts @@ -4,7 +4,12 @@ import { getRange, getSelection } from "./cross-browser"; function wrappedExceptForWhitespace(text: string, front: string, back: string): string { - const match = text.match(/^(\s*)([^]*?)(\s*)$/)!; + const normalizedText = text + .replace(/ /g, " ") + .replace(/ /g, " ") + .replace(/\u00A0/g, " "); + + const match = normalizedText.match(/^(\s*)([^]*?)(\s*)$/)!; return match[1] + front + match[2] + back + match[3]; } From 26751f220762bac08f5b06f015a03be7f55cc108 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 16 Dec 2025 00:09:51 +0800 Subject: [PATCH 3/6] fix(io): remove incorrect error toast shown when saving twice (#4458) --- .../add-or-update-note.svelte.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/ts/routes/image-occlusion/add-or-update-note.svelte.ts b/ts/routes/image-occlusion/add-or-update-note.svelte.ts index ce31eaaaf..8494563b4 100644 --- a/ts/routes/image-occlusion/add-or-update-note.svelte.ts +++ b/ts/routes/image-occlusion/add-or-update-note.svelte.ts @@ -37,7 +37,9 @@ export const addOrUpdateNote = async function( backExtra, tags, }); - showResult(mode.noteId, result, noteCount); + if (result.note) { + showResult(mode.noteId, result, noteCount); + } } else { const result = await addImageOcclusionNote({ // IOCloningMode is not used on mobile @@ -55,23 +57,12 @@ export const addOrUpdateNote = async function( // show toast message const showResult = (noteId: number | null, result: OpChanges, count: number) => { const props = $state({ - message: "", - type: "error" as "error" | "success", + message: noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count }), + type: "success" as "error" | "success", showToast: true, }); mount(Toast, { target: document.body, 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; - } }; From cb7a8dbc1022167f04be7d1669bfcfbee86d5019 Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 17 Dec 2025 00:22:18 +0800 Subject: [PATCH 4/6] fix(ci): bump cargo-deny to 0.18.6 (#4447) --- tools/minilints/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index 6d38278b5..ca645efe6 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -202,7 +202,7 @@ fn sveltekit_temp_file(path: &str) -> bool { } 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")?; Ok(()) } From 4e8c992be1b7d628ba4168a54355fedb0b0e6804 Mon Sep 17 00:00:00 2001 From: Lee Doughty <32392044+leedoughty@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:23:40 +0000 Subject: [PATCH 5/6] fix: prevent
from appearing as text in error message (#4451) * Update error text format to use RichText rather than PlainText * Set CardTypeError messages as rich text to allow HTML formatting * Use CardTypeError from anki.errors module --- qt/aqt/errors.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index a6d9251e2..89e15246e 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -14,7 +14,7 @@ from markdown import markdown import aqt 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 aqt.addons import AddonManager, AddonMeta from aqt.qt import * @@ -36,6 +36,14 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None: global _mbox error_lines = [] 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 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) 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() @@ -171,7 +179,10 @@ if not os.environ.get("DEBUG"): 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 @@ -179,7 +190,7 @@ def _init_message_box( _mbox.setWindowTitle("Anki") _mbox.setText(user_text) _mbox.setIcon(QMessageBox.Icon.Warning) - _mbox.setTextFormat(Qt.TextFormat.PlainText) + _mbox.setTextFormat(text_format) def show_help(): openHelp(help_page) From 4c7b343231bb2730ae72d6be54cb6be0a6aec3d7 Mon Sep 17 00:00:00 2001 From: GithubAnon0000 <160563432+GithubAnon0000@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:26:43 +0000 Subject: [PATCH 6/6] CHANGE: use minus sign instead of hyphen (#4437) --- qt/aqt/deckbrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 5dc688155..ca754e783 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -234,7 +234,7 @@ class DeckBrowser: if node.collapsed: prefix = "+" else: - prefix = "-" + prefix = "−" def indent() -> str: return " " * 6 * (node.level - 1)