mirror of
https://github.com/ankitects/anki.git
synced 2026-01-07 02:53:54 -05:00
Merge branch 'main' into fix/sql-retrievability-underflow
This commit is contained in:
commit
71b5c9e33d
20 changed files with 118 additions and 51 deletions
|
|
@ -254,6 +254,7 @@ nav1s <nav1s@proton.me>
|
||||||
Ranjit Odedra <ranjitodedra.dev@gmail.com>
|
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>
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 " " * 6 * (node.level - 1)
|
return " " * 6 * (node.level - 1)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@
|
||||||
<property name="insertPolicy">
|
<property name="insertPolicy">
|
||||||
<enum>QComboBox::NoInsert</enum>
|
<enum>QComboBox::NoInsert</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="sizeAdjustPolicy">
|
||||||
|
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,15 @@ class TTSPlayer:
|
||||||
|
|
||||||
rank -= 1
|
rank -= 1
|
||||||
|
|
||||||
# if no preferred voices match, we fall back on language
|
# if no requested voices match, use a preferred fallback voice
|
||||||
# with a rank of -100
|
# (for example, Apple Samantha) with rank of -50
|
||||||
|
for avail in avail_voices:
|
||||||
|
if avail.lang == tag.lang:
|
||||||
|
if avail.lang == "en_US" and avail.name.startswith("Apple_Samantha"):
|
||||||
|
return TTSVoiceMatch(voice=avail, rank=-50)
|
||||||
|
|
||||||
|
# if no requested or preferred voices match, we fall back on
|
||||||
|
# the first available voice for the language, with a rank of -100
|
||||||
for avail in avail_voices:
|
for avail in avail_voices:
|
||||||
if avail.lang == tag.lang:
|
if avail.lang == tag.lang:
|
||||||
return TTSVoiceMatch(voice=avail, rank=-100)
|
return TTSVoiceMatch(voice=avail, rank=-100)
|
||||||
|
|
|
||||||
|
|
@ -809,7 +809,7 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
||||||
wsize = widget.size()
|
wsize = widget.size()
|
||||||
cappedWidth = min(geom.width(), wsize.width())
|
cappedWidth = min(geom.width(), wsize.width())
|
||||||
cappedHeight = min(geom.height(), wsize.height())
|
cappedHeight = min(geom.height(), wsize.height())
|
||||||
if cappedWidth > wsize.width() or cappedHeight > wsize.height():
|
if cappedWidth < wsize.width() or cappedHeight < wsize.height():
|
||||||
widget.resize(QSize(cappedWidth, cappedHeight))
|
widget.resize(QSize(cappedWidth, cappedHeight))
|
||||||
|
|
||||||
# ensure widget is inside top left
|
# ensure widget is inside top left
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::import_export::package::media::SafeMediaEntry;
|
||||||
use crate::import_export::ImportProgress;
|
use crate::import_export::ImportProgress;
|
||||||
use crate::media::files::add_hash_suffix_to_file_stem;
|
use crate::media::files::add_hash_suffix_to_file_stem;
|
||||||
use crate::media::files::sha1_of_reader;
|
use crate::media::files::sha1_of_reader;
|
||||||
|
use crate::media::Checksums;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::ThrottlingProgressHandler;
|
use crate::progress::ThrottlingProgressHandler;
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ impl Context<'_> {
|
||||||
fn prepare_media(
|
fn prepare_media(
|
||||||
media_entries: Vec<SafeMediaEntry>,
|
media_entries: Vec<SafeMediaEntry>,
|
||||||
archive: &mut ZipArchive<File>,
|
archive: &mut ZipArchive<File>,
|
||||||
existing_sha1s: &HashMap<String, Sha1Hash>,
|
existing_sha1s: &Checksums,
|
||||||
progress: &mut ThrottlingProgressHandler<ImportProgress>,
|
progress: &mut ThrottlingProgressHandler<ImportProgress>,
|
||||||
) -> Result<MediaUseMap> {
|
) -> Result<MediaUseMap> {
|
||||||
let mut media_map = MediaUseMap::default();
|
let mut media_map = MediaUseMap::default();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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::collections::HashSet;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
@ -106,6 +107,8 @@ struct ColumnContext {
|
||||||
notetype_column: Option<usize>,
|
notetype_column: Option<usize>,
|
||||||
/// Source column indices for the fields of a notetype
|
/// Source column indices for the fields of a notetype
|
||||||
field_source_columns: FieldSourceColumns,
|
field_source_columns: FieldSourceColumns,
|
||||||
|
/// Metadata column indices (1-based)
|
||||||
|
meta_columns: HashSet<usize>,
|
||||||
/// How fields are converted to strings. Used for escaping HTML if
|
/// How fields are converted to strings. Used for escaping HTML if
|
||||||
/// appropriate.
|
/// appropriate.
|
||||||
stringify: fn(&str) -> String,
|
stringify: fn(&str) -> String,
|
||||||
|
|
@ -119,6 +122,7 @@ impl ColumnContext {
|
||||||
deck_column: metadata.deck()?.column(),
|
deck_column: metadata.deck()?.column(),
|
||||||
notetype_column: metadata.notetype()?.column(),
|
notetype_column: metadata.notetype()?.column(),
|
||||||
field_source_columns: metadata.field_source_columns()?,
|
field_source_columns: metadata.field_source_columns()?,
|
||||||
|
meta_columns: metadata.meta_columns(),
|
||||||
stringify: stringify_fn(metadata.is_html),
|
stringify: stringify_fn(metadata.is_html),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -166,11 +170,19 @@ impl ColumnContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_note_fields(&self, record: &csv::StringRecord) -> Vec<Option<String>> {
|
fn gather_note_fields(&self, record: &csv::StringRecord) -> Vec<Option<String>> {
|
||||||
let stringify = self.stringify;
|
let op = |i| record.get(i - 1).map(self.stringify);
|
||||||
self.field_source_columns
|
if !self.field_source_columns.is_empty() {
|
||||||
.iter()
|
self.field_source_columns
|
||||||
.map(|opt| opt.and_then(|idx| record.get(idx - 1)).map(stringify))
|
.iter()
|
||||||
.collect()
|
.map(|opt| opt.and_then(op))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
// notetype column provided, assume all non-metadata columns are notetype fields
|
||||||
|
(1..=record.len())
|
||||||
|
.filter(|i| !self.meta_columns.contains(i))
|
||||||
|
.map(op)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,11 +291,8 @@ impl CsvMetadataHelpers for CsvMetadata {
|
||||||
.map(|&i| (i > 0).then_some(i as usize))
|
.map(|&i| (i > 0).then_some(i as usize))
|
||||||
.collect(),
|
.collect(),
|
||||||
CsvNotetype::NotetypeColumn(_) => {
|
CsvNotetype::NotetypeColumn(_) => {
|
||||||
let meta_columns = self.meta_columns();
|
// each row's notetype could have varying number of fields
|
||||||
(1..self.column_labels.len() + 1)
|
vec![]
|
||||||
.filter(|idx| !meta_columns.contains(idx))
|
|
||||||
.map(Some)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,9 @@ pub fn add_data_to_folder_uniquely<'a, P>(
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
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());
|
let mut target_path = folder.as_ref().join(normalized_name.as_ref());
|
||||||
|
|
||||||
|
|
@ -496,8 +498,14 @@ mod test {
|
||||||
"test.mp3"
|
"test.mp3"
|
||||||
);
|
);
|
||||||
|
|
||||||
// different contents
|
// different contents, filenames differ only by case
|
||||||
let h2 = sha1_of_data(b"hello1");
|
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!(
|
assert_eq!(
|
||||||
add_data_to_folder_uniquely(dpath, "test.mp3", b"hello1", h2).unwrap(),
|
add_data_to_folder_uniquely(dpath, "test.mp3", b"hello1", h2).unwrap(),
|
||||||
"test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3"
|
"test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ pub mod files;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
@ -22,6 +21,7 @@ use crate::progress::ThrottlingProgressHandler;
|
||||||
use crate::sync::http_client::HttpSyncClient;
|
use crate::sync::http_client::HttpSyncClient;
|
||||||
use crate::sync::login::SyncAuth;
|
use crate::sync::login::SyncAuth;
|
||||||
use crate::sync::media::database::client::changetracker::ChangeTracker;
|
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::MediaDatabase;
|
||||||
use crate::sync::media::database::client::MediaEntry;
|
use crate::sync::media::database::client::MediaEntry;
|
||||||
use crate::sync::media::progress::MediaSyncProgress;
|
use crate::sync::media::progress::MediaSyncProgress;
|
||||||
|
|
@ -157,7 +157,7 @@ impl MediaManager {
|
||||||
pub fn all_checksums_after_checking(
|
pub fn all_checksums_after_checking(
|
||||||
&self,
|
&self,
|
||||||
progress: impl FnMut(usize) -> bool,
|
progress: impl FnMut(usize) -> bool,
|
||||||
) -> Result<HashMap<String, Sha1Hash>> {
|
) -> Result<Checksums> {
|
||||||
ChangeTracker::new(&self.media_folder, progress).register_changes(&self.db)?;
|
ChangeTracker::new(&self.media_folder, progress).register_changes(&self.db)?;
|
||||||
self.db.all_registered_checksums()
|
self.db.all_registered_checksums()
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ impl MediaManager {
|
||||||
|
|
||||||
/// All checksums without registering changes first.
|
/// All checksums without registering changes first.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn all_checksums_as_is(&self) -> HashMap<String, [u8; 20]> {
|
pub(crate) fn all_checksums_as_is(&self) -> Checksums {
|
||||||
self.db.all_registered_checksums().unwrap()
|
self.db.all_registered_checksums().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -443,9 +443,20 @@ impl Collection {
|
||||||
.storage
|
.storage
|
||||||
.get_deck(card.deck_id)?
|
.get_deck(card.deck_id)?
|
||||||
.or_not_found(card.deck_id)?;
|
.or_not_found(card.deck_id)?;
|
||||||
let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?;
|
let home_deck = if card.original_deck_id.0 == 0 {
|
||||||
|
&deck
|
||||||
|
} else {
|
||||||
|
&self
|
||||||
|
.storage
|
||||||
|
.get_deck(card.original_deck_id)?
|
||||||
|
.or_not_found(card.original_deck_id)?
|
||||||
|
};
|
||||||
|
let config = self
|
||||||
|
.storage
|
||||||
|
.get_deck_config(home_deck.config_id().or_invalid("home deck is filtered")?)?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let desired_retention = deck.effective_desired_retention(&config);
|
let desired_retention = home_deck.effective_desired_retention(&config);
|
||||||
let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs);
|
let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs);
|
||||||
let fsrs_next_states = if fsrs_enabled {
|
let fsrs_next_states = if fsrs_enabled {
|
||||||
let params = config.fsrs_params();
|
let params = config.fsrs_params();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ use crate::prelude::*;
|
||||||
|
|
||||||
pub mod changetracker;
|
pub mod changetracker;
|
||||||
|
|
||||||
|
pub struct Checksums(HashMap<String, Sha1Hash>);
|
||||||
|
|
||||||
|
impl Checksums {
|
||||||
|
// case-fold filenames when checking files to be imported
|
||||||
|
// to account for case-insensitive filesystems
|
||||||
|
pub fn get(&self, key: impl AsRef<str>) -> Option<&Sha1Hash> {
|
||||||
|
self.0.get(key.as_ref().to_lowercase().as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
|
||||||
|
self.get(key).is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct MediaEntry {
|
pub struct MediaEntry {
|
||||||
pub fname: String,
|
pub fname: String,
|
||||||
|
|
@ -175,11 +189,12 @@ delete from media where fname=?",
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all filenames and checksums, where the checksum is not null.
|
/// Returns all filenames and checksums, where the checksum is not null.
|
||||||
pub(crate) fn all_registered_checksums(&self) -> error::Result<HashMap<String, Sha1Hash>> {
|
pub(crate) fn all_registered_checksums(&self) -> error::Result<Checksums> {
|
||||||
self.db
|
self.db
|
||||||
.prepare("SELECT fname, csum FROM media WHERE csum IS NOT NULL")?
|
.prepare("SELECT fname, csum FROM media WHERE csum IS NOT NULL")?
|
||||||
.query_and_then([], row_to_name_and_checksum)?
|
.query_and_then([], row_to_name_and_checksum)?
|
||||||
.collect()
|
.collect::<error::Result<_>>()
|
||||||
|
.map(Checksums)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn force_resync(&self) -> error::Result<()> {
|
pub(crate) fn force_resync(&self) -> error::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@
|
||||||
import { getRange, getSelection } from "./cross-browser";
|
import { getRange, getSelection } from "./cross-browser";
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
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];
|
return match[1] + front + match[2] + back + match[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue