mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Merge branch 'main' into color-palette
This commit is contained in:
commit
bbab485373
16 changed files with 209 additions and 66 deletions
|
@ -46,3 +46,5 @@ preferences-daily-backups = Daily backups to keep:
|
||||||
preferences-weekly-backups = Weekly backups to keep:
|
preferences-weekly-backups = Weekly backups to keep:
|
||||||
preferences-monthly-backups = Monthly backups to keep:
|
preferences-monthly-backups = Monthly backups to keep:
|
||||||
preferences-minutes-between-backups = Minutes between automatic backups:
|
preferences-minutes-between-backups = Minutes between automatic backups:
|
||||||
|
preferences-reduce-motion = Reduce motion
|
||||||
|
preferences-reduce-motion-tooltip = Disable various animations and transitions of the user interface
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>640</width>
|
<width>640</width>
|
||||||
<height>530</height>
|
<height>640</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -42,6 +42,9 @@
|
||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>12</number>
|
<number>12</number>
|
||||||
</property>
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="theme"/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="lang">
|
<widget class="QComboBox" name="lang">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
|
@ -55,9 +58,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="theme"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="video_driver"/>
|
<widget class="QComboBox" name="video_driver"/>
|
||||||
</item>
|
</item>
|
||||||
|
@ -103,6 +103,16 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="reduce_motion">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>preferences_reduce_motion_tooltip</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>preferences_reduce_motion</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="useCurrent">
|
<widget class="QComboBox" name="useCurrent">
|
||||||
<item>
|
<item>
|
||||||
|
@ -676,8 +686,8 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>lang</tabstop>
|
|
||||||
<tabstop>theme</tabstop>
|
<tabstop>theme</tabstop>
|
||||||
|
<tabstop>lang</tabstop>
|
||||||
<tabstop>video_driver</tabstop>
|
<tabstop>video_driver</tabstop>
|
||||||
<tabstop>showPlayButtons</tabstop>
|
<tabstop>showPlayButtons</tabstop>
|
||||||
<tabstop>interrupt_audio</tabstop>
|
<tabstop>interrupt_audio</tabstop>
|
||||||
|
@ -685,6 +695,7 @@
|
||||||
<tabstop>paste_strips_formatting</tabstop>
|
<tabstop>paste_strips_formatting</tabstop>
|
||||||
<tabstop>ignore_accents_in_search</tabstop>
|
<tabstop>ignore_accents_in_search</tabstop>
|
||||||
<tabstop>legacy_import_export</tabstop>
|
<tabstop>legacy_import_export</tabstop>
|
||||||
|
<tabstop>reduce_motion</tabstop>
|
||||||
<tabstop>useCurrent</tabstop>
|
<tabstop>useCurrent</tabstop>
|
||||||
<tabstop>default_search_text</tabstop>
|
<tabstop>default_search_text</tabstop>
|
||||||
<tabstop>uiScale</tabstop>
|
<tabstop>uiScale</tabstop>
|
||||||
|
@ -703,11 +714,11 @@
|
||||||
<tabstop>fullSync</tabstop>
|
<tabstop>fullSync</tabstop>
|
||||||
<tabstop>syncDeauth</tabstop>
|
<tabstop>syncDeauth</tabstop>
|
||||||
<tabstop>media_log</tabstop>
|
<tabstop>media_log</tabstop>
|
||||||
<tabstop>tabWidget</tabstop>
|
|
||||||
<tabstop>minutes_between_backups</tabstop>
|
<tabstop>minutes_between_backups</tabstop>
|
||||||
<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>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|
|
@ -207,6 +207,7 @@ class Preferences(QDialog):
|
||||||
|
|
||||||
def setup_global(self) -> None:
|
def setup_global(self) -> None:
|
||||||
"Setup options global to all profiles."
|
"Setup options global to all profiles."
|
||||||
|
self.form.reduce_motion.setChecked(self.mw.pm.reduced_motion())
|
||||||
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
|
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
|
||||||
themes = [
|
themes = [
|
||||||
tr.preferences_theme_label(theme=theme)
|
tr.preferences_theme_label(theme=theme)
|
||||||
|
@ -236,6 +237,8 @@ class Preferences(QDialog):
|
||||||
self.mw.pm.setUiScale(newScale)
|
self.mw.pm.setUiScale(newScale)
|
||||||
restart_required = True
|
restart_required = True
|
||||||
|
|
||||||
|
self.mw.pm.set_reduced_motion(self.form.reduce_motion.isChecked())
|
||||||
|
|
||||||
self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
|
self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
|
||||||
|
|
||||||
if restart_required:
|
if restart_required:
|
||||||
|
|
|
@ -518,6 +518,12 @@ create table if not exists profiles
|
||||||
def setUiScale(self, scale: float) -> None:
|
def setUiScale(self, scale: float) -> None:
|
||||||
self.meta["uiScale"] = scale
|
self.meta["uiScale"] = scale
|
||||||
|
|
||||||
|
def reduced_motion(self) -> bool:
|
||||||
|
return self.meta.get("reduced_motion", False)
|
||||||
|
|
||||||
|
def set_reduced_motion(self, on: bool) -> None:
|
||||||
|
self.meta["reduced_motion"] = on
|
||||||
|
|
||||||
def last_addon_update_check(self) -> int:
|
def last_addon_update_check(self) -> int:
|
||||||
return self.meta.get("last_addon_update_check", 0)
|
return self.meta.get("last_addon_update_check", 0)
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@ class ThemeManager:
|
||||||
return cache.setdefault(path, icon)
|
return cache.setdefault(path, icon)
|
||||||
|
|
||||||
def body_class(self, night_mode: bool | None = None) -> str:
|
def body_class(self, night_mode: bool | None = None) -> str:
|
||||||
"Returns space-separated class list for platform/theme."
|
"Returns space-separated class list for platform/theme/global settings."
|
||||||
classes = []
|
classes = []
|
||||||
if is_win:
|
if is_win:
|
||||||
classes.append("isWin")
|
classes.append("isWin")
|
||||||
|
@ -137,6 +137,8 @@ class ThemeManager:
|
||||||
classes.extend(["nightMode", "night_mode"])
|
classes.extend(["nightMode", "night_mode"])
|
||||||
if self.macos_dark_mode():
|
if self.macos_dark_mode():
|
||||||
classes.append("macos-dark-mode")
|
classes.append("macos-dark-mode")
|
||||||
|
if aqt.mw.pm.reduced_motion():
|
||||||
|
classes.append("reduced-motion")
|
||||||
return " ".join(classes)
|
return " ".join(classes)
|
||||||
|
|
||||||
def body_classes_for_card_ord(
|
def body_classes_for_card_ord(
|
||||||
|
|
|
@ -42,7 +42,7 @@ impl ExchangeData {
|
||||||
self.notes = notes;
|
self.notes = notes;
|
||||||
let (cards, guard) = guard.col.gather_cards()?;
|
let (cards, guard) = guard.col.gather_cards()?;
|
||||||
self.cards = cards;
|
self.cards = cards;
|
||||||
self.decks = guard.col.gather_decks()?;
|
self.decks = guard.col.gather_decks(with_scheduling)?;
|
||||||
self.notetypes = guard.col.gather_notetypes()?;
|
self.notetypes = guard.col.gather_notetypes()?;
|
||||||
self.check_ids()?;
|
self.check_ids()?;
|
||||||
|
|
||||||
|
@ -191,13 +191,21 @@ impl Collection {
|
||||||
.map(|cards| (cards, guard))
|
.map(|cards| (cards, guard))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gather_decks(&mut self) -> Result<Vec<Deck>> {
|
/// If with_scheduling, also gather all original decks of cards in filtered
|
||||||
let decks = self.storage.get_decks_for_search_cards()?;
|
/// decks, so they don't have to be converted to regular decks on import.
|
||||||
|
/// If not with_scheduling, skip exporting the default deck to avoid changing
|
||||||
|
/// the importing client's defaults.
|
||||||
|
fn gather_decks(&mut self, with_scheduling: bool) -> Result<Vec<Deck>> {
|
||||||
|
let decks = if with_scheduling {
|
||||||
|
self.storage.get_decks_and_original_for_search_cards()
|
||||||
|
} else {
|
||||||
|
self.storage.get_decks_for_search_cards()
|
||||||
|
}?;
|
||||||
let parents = self.get_parent_decks(&decks)?;
|
let parents = self.get_parent_decks(&decks)?;
|
||||||
Ok(decks
|
Ok(decks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|deck| deck.id != DeckId(1))
|
|
||||||
.chain(parents)
|
.chain(parents)
|
||||||
|
.filter(|deck| with_scheduling || deck.id != DeckId(1))
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +251,6 @@ impl Collection {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|deck| deck.config_id())
|
.filter_map(|deck| deck.config_id())
|
||||||
.unique()
|
.unique()
|
||||||
.filter(|config_id| *config_id != DeckConfigId(1))
|
|
||||||
.map(|config_id| {
|
.map(|config_id| {
|
||||||
self.storage
|
self.storage
|
||||||
.get_deck_config(config_id)?
|
.get_deck_config(config_id)?
|
||||||
|
|
|
@ -70,6 +70,7 @@ impl Context<'_> {
|
||||||
&mut self,
|
&mut self,
|
||||||
imported_notes: &HashMap<NoteId, NoteId>,
|
imported_notes: &HashMap<NoteId, NoteId>,
|
||||||
imported_decks: &HashMap<DeckId, DeckId>,
|
imported_decks: &HashMap<DeckId, DeckId>,
|
||||||
|
keep_filtered: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut ctx = CardContext::new(
|
let mut ctx = CardContext::new(
|
||||||
self.usn,
|
self.usn,
|
||||||
|
@ -78,16 +79,16 @@ impl Context<'_> {
|
||||||
imported_notes,
|
imported_notes,
|
||||||
imported_decks,
|
imported_decks,
|
||||||
)?;
|
)?;
|
||||||
ctx.import_cards(mem::take(&mut self.data.cards))?;
|
ctx.import_cards(mem::take(&mut self.data.cards), keep_filtered)?;
|
||||||
ctx.import_revlog(mem::take(&mut self.data.revlog))
|
ctx.import_revlog(mem::take(&mut self.data.revlog))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardContext<'_> {
|
impl CardContext<'_> {
|
||||||
fn import_cards(&mut self, mut cards: Vec<Card>) -> Result<()> {
|
fn import_cards(&mut self, mut cards: Vec<Card>, keep_filtered: bool) -> Result<()> {
|
||||||
for card in &mut cards {
|
for card in &mut cards {
|
||||||
if self.map_to_imported_note(card) && !self.card_ordinal_already_exists(card) {
|
if self.map_to_imported_note(card) && !self.card_ordinal_already_exists(card) {
|
||||||
self.add_card(card)?;
|
self.add_card(card, keep_filtered)?;
|
||||||
}
|
}
|
||||||
// TODO: could update existing card
|
// TODO: could update existing card
|
||||||
}
|
}
|
||||||
|
@ -119,11 +120,13 @@ impl CardContext<'_> {
|
||||||
.contains(&(card.note_id, card.template_idx))
|
.contains(&(card.note_id, card.template_idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_card(&mut self, card: &mut Card) -> Result<()> {
|
fn add_card(&mut self, card: &mut Card, keep_filtered: bool) -> Result<()> {
|
||||||
card.usn = self.usn;
|
card.usn = self.usn;
|
||||||
self.remap_deck_id(card);
|
self.remap_deck_ids(card);
|
||||||
card.shift_collection_relative_dates(self.collection_delta);
|
card.shift_collection_relative_dates(self.collection_delta);
|
||||||
card.maybe_remove_from_filtered_deck(self.scheduler_version);
|
if !keep_filtered {
|
||||||
|
card.maybe_remove_from_filtered_deck(self.scheduler_version);
|
||||||
|
}
|
||||||
let old_id = self.uniquify_card_id(card);
|
let old_id = self.uniquify_card_id(card);
|
||||||
|
|
||||||
self.target_col.add_card_if_unique_undoable(card)?;
|
self.target_col.add_card_if_unique_undoable(card)?;
|
||||||
|
@ -141,10 +144,13 @@ impl CardContext<'_> {
|
||||||
original
|
original
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remap_deck_id(&self, card: &mut Card) {
|
fn remap_deck_ids(&self, card: &mut Card) {
|
||||||
if let Some(did) = self.remapped_decks.get(&card.deck_id) {
|
if let Some(did) = self.remapped_decks.get(&card.deck_id) {
|
||||||
card.deck_id = *did;
|
card.deck_id = *did;
|
||||||
}
|
}
|
||||||
|
if let Some(did) = self.remapped_decks.get(&card.original_deck_id) {
|
||||||
|
card.original_deck_id = *did;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,13 @@ impl<'d> DeckContext<'d> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context<'_> {
|
impl Context<'_> {
|
||||||
pub(super) fn import_decks_and_configs(&mut self) -> Result<HashMap<DeckId, DeckId>> {
|
pub(super) fn import_decks_and_configs(
|
||||||
|
&mut self,
|
||||||
|
keep_filtered: bool,
|
||||||
|
) -> Result<HashMap<DeckId, DeckId>> {
|
||||||
let mut ctx = DeckContext::new(self.target_col, self.usn);
|
let mut ctx = DeckContext::new(self.target_col, self.usn);
|
||||||
ctx.import_deck_configs(mem::take(&mut self.data.deck_configs))?;
|
ctx.import_deck_configs(mem::take(&mut self.data.deck_configs))?;
|
||||||
ctx.import_decks(mem::take(&mut self.data.decks))?;
|
ctx.import_decks(mem::take(&mut self.data.decks), keep_filtered)?;
|
||||||
Ok(ctx.imported_decks)
|
Ok(ctx.imported_decks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,19 +47,19 @@ impl DeckContext<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_decks(&mut self, mut decks: Vec<Deck>) -> Result<()> {
|
fn import_decks(&mut self, mut decks: Vec<Deck>, keep_filtered: bool) -> Result<()> {
|
||||||
// ensure parents are seen before children
|
// ensure parents are seen before children
|
||||||
decks.sort_unstable_by_key(|deck| deck.level());
|
decks.sort_unstable_by_key(|deck| deck.level());
|
||||||
for deck in &mut decks {
|
for deck in &mut decks {
|
||||||
self.prepare_deck(deck);
|
self.prepare_deck(deck, keep_filtered);
|
||||||
self.import_deck(deck)?;
|
self.import_deck(deck)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_deck(&mut self, deck: &mut Deck) {
|
fn prepare_deck(&self, deck: &mut Deck, keep_filtered: bool) {
|
||||||
self.maybe_reparent(deck);
|
self.maybe_reparent(deck);
|
||||||
if deck.is_filtered() {
|
if !keep_filtered && deck.is_filtered() {
|
||||||
deck.kind = DeckKind::Normal(NormalDeck {
|
deck.kind = DeckKind::Normal(NormalDeck {
|
||||||
config_id: 1,
|
config_id: 1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -66,16 +69,14 @@ impl DeckContext<'_> {
|
||||||
|
|
||||||
fn import_deck(&mut self, deck: &mut Deck) -> Result<()> {
|
fn import_deck(&mut self, deck: &mut Deck) -> Result<()> {
|
||||||
if let Some(original) = self.get_deck_by_name(deck)? {
|
if let Some(original) = self.get_deck_by_name(deck)? {
|
||||||
if original.is_filtered() {
|
if original.is_same_kind(deck) {
|
||||||
self.uniquify_name(deck);
|
return self.update_deck(deck, original);
|
||||||
self.add_deck(deck)
|
|
||||||
} else {
|
} else {
|
||||||
self.update_deck(deck, original)
|
self.uniquify_name(deck);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.ensure_valid_first_existing_parent(deck)?;
|
|
||||||
self.add_deck(deck)
|
|
||||||
}
|
}
|
||||||
|
self.ensure_valid_first_existing_parent(deck)?;
|
||||||
|
self.add_deck(deck)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_reparent(&self, deck: &mut Deck) {
|
fn maybe_reparent(&self, deck: &mut Deck) {
|
||||||
|
@ -113,10 +114,16 @@ impl DeckContext<'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Caller must ensure decks are normal.
|
/// Caller must ensure decks are of the same kind.
|
||||||
fn update_deck(&mut self, deck: &Deck, original: Deck) -> Result<()> {
|
fn update_deck(&mut self, deck: &Deck, original: Deck) -> Result<()> {
|
||||||
let mut new_deck = original.clone();
|
let mut new_deck = original.clone();
|
||||||
new_deck.normal_mut()?.update_with_other(deck.normal()?);
|
if let (Ok(new), Ok(old)) = (new_deck.normal_mut(), deck.normal()) {
|
||||||
|
new.update_with_other(old);
|
||||||
|
} else if let (Ok(new), Ok(old)) = (new_deck.filtered_mut(), deck.filtered()) {
|
||||||
|
*new = old.clone();
|
||||||
|
} else {
|
||||||
|
return Err(AnkiError::invalid_input("decks have different kinds"));
|
||||||
|
}
|
||||||
self.imported_decks.insert(deck.id, new_deck.id);
|
self.imported_decks.insert(deck.id, new_deck.id);
|
||||||
self.target_col
|
self.target_col
|
||||||
.update_deck_inner(&mut new_deck, original, self.usn)
|
.update_deck_inner(&mut new_deck, original, self.usn)
|
||||||
|
@ -152,6 +159,10 @@ impl Deck {
|
||||||
fn level(&self) -> usize {
|
fn level(&self) -> usize {
|
||||||
self.name.components().count()
|
self.name.components().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_same_kind(&self, other: &Self) -> bool {
|
||||||
|
self.is_filtered() == other.is_filtered()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NormalDeck {
|
impl NormalDeck {
|
||||||
|
@ -194,7 +205,7 @@ mod test {
|
||||||
new_deck_with_machine_name("NEW PARENT\x1fchild", false),
|
new_deck_with_machine_name("NEW PARENT\x1fchild", false),
|
||||||
new_deck_with_machine_name("new parent", false),
|
new_deck_with_machine_name("new parent", false),
|
||||||
];
|
];
|
||||||
ctx.import_decks(imports).unwrap();
|
ctx.import_decks(imports, false).unwrap();
|
||||||
let existing_decks: HashSet<_> = ctx
|
let existing_decks: HashSet<_> = ctx
|
||||||
.target_col
|
.target_col
|
||||||
.get_all_deck_names(true)
|
.get_all_deck_names(true)
|
||||||
|
|
|
@ -6,7 +6,7 @@ mod decks;
|
||||||
mod media;
|
mod media;
|
||||||
mod notes;
|
mod notes;
|
||||||
|
|
||||||
use std::{fs::File, io, path::Path};
|
use std::{collections::HashSet, fs::File, io, path::Path};
|
||||||
|
|
||||||
pub(crate) use notes::NoteMeta;
|
pub(crate) use notes::NoteMeta;
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
|
@ -82,8 +82,9 @@ impl<'a> Context<'a> {
|
||||||
fn import(&mut self) -> Result<NoteLog> {
|
fn import(&mut self) -> Result<NoteLog> {
|
||||||
let mut media_map = self.prepare_media()?;
|
let mut media_map = self.prepare_media()?;
|
||||||
let note_imports = self.import_notes_and_notetypes(&mut media_map)?;
|
let note_imports = self.import_notes_and_notetypes(&mut media_map)?;
|
||||||
let imported_decks = self.import_decks_and_configs()?;
|
let keep_filtered = self.data.enables_filtered_decks();
|
||||||
self.import_cards_and_revlog(¬e_imports.id_map, &imported_decks)?;
|
let imported_decks = self.import_decks_and_configs(keep_filtered)?;
|
||||||
|
self.import_cards_and_revlog(¬e_imports.id_map, &imported_decks, keep_filtered)?;
|
||||||
self.copy_media(&mut media_map)?;
|
self.copy_media(&mut media_map)?;
|
||||||
Ok(note_imports.log)
|
Ok(note_imports.log)
|
||||||
}
|
}
|
||||||
|
@ -107,6 +108,24 @@ impl ExchangeData {
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enables_filtered_decks(&self) -> bool {
|
||||||
|
// Earlier versions relied on the importer handling filtered decks by converting
|
||||||
|
// them into regular ones, so there is no guarantee that all original decks
|
||||||
|
// are included.
|
||||||
|
self.contains_scheduling() && self.contains_all_original_decks()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_scheduling(&self) -> bool {
|
||||||
|
!(self.revlog.is_empty() && self.deck_configs.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_all_original_decks(&self) -> bool {
|
||||||
|
let deck_ids: HashSet<_> = self.decks.iter().map(|d| d.id).collect();
|
||||||
|
self.cards
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.original_deck_id.0 == 0 || deck_ids.contains(&c.original_deck_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collection_to_tempfile(meta: &Meta, archive: &mut ZipArchive<File>) -> Result<NamedTempFile> {
|
fn collection_to_tempfile(meta: &Meta, archive: &mut ZipArchive<File>) -> Result<NamedTempFile> {
|
||||||
|
|
|
@ -158,10 +158,10 @@ impl ColumnContext {
|
||||||
|
|
||||||
fn foreign_note_from_record(&self, record: &csv::StringRecord) -> ForeignNote {
|
fn foreign_note_from_record(&self, record: &csv::StringRecord) -> ForeignNote {
|
||||||
ForeignNote {
|
ForeignNote {
|
||||||
notetype: str_from_record_column(self.notetype_column, record).into(),
|
notetype: name_or_id_from_record_column(self.notetype_column, record),
|
||||||
fields: self.gather_note_fields(record),
|
fields: self.gather_note_fields(record),
|
||||||
tags: self.gather_tags(record),
|
tags: self.gather_tags(record),
|
||||||
deck: str_from_record_column(self.deck_column, record).into(),
|
deck: name_or_id_from_record_column(self.deck_column, record),
|
||||||
guid: str_from_record_column(self.guid_column, record),
|
guid: str_from_record_column(self.guid_column, record),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
@ -192,6 +192,10 @@ fn str_from_record_column(column: Option<usize>, record: &csv::StringRecord) ->
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn name_or_id_from_record_column(column: Option<usize>, record: &csv::StringRecord) -> NameOrId {
|
||||||
|
NameOrId::parse(column.and_then(|i| record.get(i - 1)).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build_csv_reader(
|
pub(super) fn build_csv_reader(
|
||||||
mut reader: impl Read + Seek,
|
mut reader: impl Read + Seek,
|
||||||
delimiter: Delimiter,
|
delimiter: Delimiter,
|
||||||
|
|
|
@ -67,7 +67,6 @@ impl Collection {
|
||||||
maybe_set_fallback_columns(&mut metadata)?;
|
maybe_set_fallback_columns(&mut metadata)?;
|
||||||
self.maybe_set_fallback_notetype(&mut metadata, notetype_id)?;
|
self.maybe_set_fallback_notetype(&mut metadata, notetype_id)?;
|
||||||
self.maybe_init_notetype_map(&mut metadata)?;
|
self.maybe_init_notetype_map(&mut metadata)?;
|
||||||
maybe_set_tags_column(&mut metadata);
|
|
||||||
self.maybe_set_fallback_deck(&mut metadata)?;
|
self.maybe_set_fallback_deck(&mut metadata)?;
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
|
@ -222,6 +221,7 @@ impl Collection {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ensure_first_field_is_mapped(&mut global.field_columns, column_len, &meta_columns)?;
|
ensure_first_field_is_mapped(&mut global.field_columns, column_len, &meta_columns)?;
|
||||||
|
maybe_set_tags_column(metadata, &meta_columns);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -388,12 +388,15 @@ fn maybe_set_fallback_delimiter(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_set_tags_column(metadata: &mut CsvMetadata) {
|
fn maybe_set_tags_column(metadata: &mut CsvMetadata, meta_columns: &HashSet<usize>) {
|
||||||
if metadata.tags_column == 0 {
|
if metadata.tags_column == 0 {
|
||||||
if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype {
|
if let Some(CsvNotetype::GlobalNotetype(ref global)) = metadata.notetype {
|
||||||
let max_field = global.field_columns.iter().max().copied().unwrap_or(0);
|
let max_field = global.field_columns.iter().max().copied().unwrap_or(0);
|
||||||
if max_field < metadata.column_labels.len() as u32 {
|
for idx in (max_field + 1) as usize..metadata.column_labels.len() {
|
||||||
metadata.tags_column = max_field + 1;
|
if !meta_columns.contains(&idx) {
|
||||||
|
metadata.tags_column = max_field + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,6 +129,11 @@ impl DeckIdsByNameOrId {
|
||||||
NameOrId::Name(name) => self.names.get(name).copied(),
|
NameOrId::Name(name) => self.names.get(name).copied(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, deck_id: DeckId, name: String) {
|
||||||
|
self.ids.insert(deck_id);
|
||||||
|
self.names.insert(name, deck_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Context<'a> {
|
impl<'a> Context<'a> {
|
||||||
|
@ -195,7 +200,7 @@ impl<'a> Context<'a> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(notetype) = self.notetype_for_note(&foreign)? {
|
if let Some(notetype) = self.notetype_for_note(&foreign)? {
|
||||||
if let Some(deck_id) = self.deck_ids.get(&foreign.deck) {
|
if let Some(deck_id) = self.get_or_create_deck_id(&foreign.deck)? {
|
||||||
let ctx = self.build_note_context(
|
let ctx = self.build_note_context(
|
||||||
foreign,
|
foreign,
|
||||||
notetype,
|
notetype,
|
||||||
|
@ -214,6 +219,20 @@ impl<'a> Context<'a> {
|
||||||
Ok(log)
|
Ok(log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_or_create_deck_id(&mut self, deck: &NameOrId) -> Result<Option<DeckId>> {
|
||||||
|
Ok(if let Some(did) = self.deck_ids.get(deck) {
|
||||||
|
Some(did)
|
||||||
|
} else if let NameOrId::Name(name) = deck {
|
||||||
|
let mut deck = Deck::new_normal();
|
||||||
|
deck.name = NativeDeckName::from_human_name(name);
|
||||||
|
self.col.add_deck_inner(&mut deck, self.usn)?;
|
||||||
|
self.deck_ids.insert(deck.id, deck.human_name());
|
||||||
|
Some(deck.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn build_note_context<'tags>(
|
fn build_note_context<'tags>(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut note: ForeignNote,
|
mut note: ForeignNote,
|
||||||
|
@ -240,16 +259,17 @@ impl<'a> Context<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_duplicates(&self, notetype: &Notetype, note: &ForeignNote) -> Result<Vec<Duplicate>> {
|
fn find_duplicates(&self, notetype: &Notetype, note: &ForeignNote) -> Result<Vec<Duplicate>> {
|
||||||
if let Some(nid) = self.existing_guids.get(¬e.guid) {
|
if note.guid.is_empty() {
|
||||||
self.get_guid_dupe(*nid, note).map(|dupe| vec![dupe])
|
if let Some(nids) = note
|
||||||
} else if let Some(nids) = note
|
.checksum()
|
||||||
.checksum()
|
.and_then(|csum| self.existing_checksums.get(&(notetype.id, csum)))
|
||||||
.and_then(|csum| self.existing_checksums.get(&(notetype.id, csum)))
|
{
|
||||||
{
|
return self.get_first_field_dupes(note, nids);
|
||||||
self.get_first_field_dupes(note, nids)
|
}
|
||||||
} else {
|
} else if let Some(nid) = self.existing_guids.get(¬e.guid) {
|
||||||
Ok(Vec::new())
|
return self.get_guid_dupe(*nid, note).map(|dupe| vec![dupe]);
|
||||||
}
|
}
|
||||||
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_guid_dupe(&self, nid: NoteId, original: &ForeignNote) -> Result<Duplicate> {
|
fn get_guid_dupe(&self, nid: NoteId, original: &ForeignNote) -> Result<Duplicate> {
|
||||||
|
@ -271,20 +291,21 @@ impl<'a> Context<'a> {
|
||||||
|
|
||||||
fn import_note(&mut self, ctx: NoteContext, log: &mut NoteLog) -> Result<()> {
|
fn import_note(&mut self, ctx: NoteContext, log: &mut NoteLog) -> Result<()> {
|
||||||
match self.dupe_resolution {
|
match self.dupe_resolution {
|
||||||
_ if ctx.dupes.is_empty() => self.add_note(ctx, log, false)?,
|
_ if !ctx.is_dupe() => self.add_note(ctx, log)?,
|
||||||
DupeResolution::Add => self.add_note(ctx, log, true)?,
|
DupeResolution::Add if ctx.is_guid_dupe() => {
|
||||||
|
log.duplicate.push(ctx.note.into_log_note())
|
||||||
|
}
|
||||||
|
DupeResolution::Add if !ctx.has_first_field() => {
|
||||||
|
log.empty_first_field.push(ctx.note.into_log_note())
|
||||||
|
}
|
||||||
|
DupeResolution::Add => self.add_note(ctx, log)?,
|
||||||
DupeResolution::Update => self.update_with_note(ctx, log)?,
|
DupeResolution::Update => self.update_with_note(ctx, log)?,
|
||||||
DupeResolution::Ignore => log.first_field_match.push(ctx.note.into_log_note()),
|
DupeResolution::Ignore => log.first_field_match.push(ctx.note.into_log_note()),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_note(&mut self, ctx: NoteContext, log: &mut NoteLog, dupe: bool) -> Result<()> {
|
fn add_note(&mut self, ctx: NoteContext, log: &mut NoteLog) -> Result<()> {
|
||||||
if !ctx.note.first_field_is_unempty() {
|
|
||||||
log.empty_first_field.push(ctx.note.into_log_note());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut note = Note::new(&ctx.notetype);
|
let mut note = Note::new(&ctx.notetype);
|
||||||
let mut cards = ctx
|
let mut cards = ctx
|
||||||
.note
|
.note
|
||||||
|
@ -293,10 +314,10 @@ impl<'a> Context<'a> {
|
||||||
self.col.add_note_only_undoable(&mut note)?;
|
self.col.add_note_only_undoable(&mut note)?;
|
||||||
self.add_cards(&mut cards, ¬e, ctx.deck_id, ctx.notetype)?;
|
self.add_cards(&mut cards, ¬e, ctx.deck_id, ctx.notetype)?;
|
||||||
|
|
||||||
if dupe {
|
if ctx.dupes.is_empty() {
|
||||||
log.first_field_match.push(note.into_log_note());
|
|
||||||
} else {
|
|
||||||
log.new.push(note.into_log_note());
|
log.new.push(note.into_log_note());
|
||||||
|
} else {
|
||||||
|
log.first_field_match.push(note.into_log_note());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -376,6 +397,22 @@ impl<'a> Context<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NoteContext<'_> {
|
||||||
|
fn is_dupe(&self) -> bool {
|
||||||
|
!self.dupes.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_guid_dupe(&self) -> bool {
|
||||||
|
self.dupes
|
||||||
|
.get(0)
|
||||||
|
.map_or(false, |d| d.note.guid == self.note.guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_first_field(&self) -> bool {
|
||||||
|
self.note.first_field_is_unempty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Note {
|
impl Note {
|
||||||
fn first_field_stripped(&self) -> Cow<str> {
|
fn first_field_stripped(&self) -> Cow<str> {
|
||||||
strip_html_preserving_media_filenames(&self.fields()[0])
|
strip_html_preserving_media_filenames(&self.fields()[0])
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
WITH cids AS (
|
||||||
|
SELECT cid
|
||||||
|
FROM search_cids
|
||||||
|
)
|
||||||
|
SELECT did
|
||||||
|
FROM cards
|
||||||
|
WHERE id IN cids
|
||||||
|
UNION
|
||||||
|
SELECT odid
|
||||||
|
FROM cards
|
||||||
|
WHERE odid != 0
|
||||||
|
AND id IN cids
|
|
@ -131,6 +131,18 @@ impl SqliteStorage {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_decks_and_original_for_search_cards(&self) -> Result<Vec<Deck>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached(concat!(
|
||||||
|
include_str!("get_deck.sql"),
|
||||||
|
" WHERE id IN (",
|
||||||
|
include_str!("all_decks_and_original_of_search_cards.sql"),
|
||||||
|
")",
|
||||||
|
))?
|
||||||
|
.query_and_then([], row_to_deck)?
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the deck id of the first existing card of every searched note.
|
/// Returns the deck id of the first existing card of every searched note.
|
||||||
pub(crate) fn all_decks_of_search_notes(&self) -> Result<HashMap<NoteId, DeckId>> {
|
pub(crate) fn all_decks_of_search_notes(&self) -> Result<HashMap<NoteId, DeckId>> {
|
||||||
self.db
|
self.db
|
||||||
|
|
|
@ -74,3 +74,8 @@ samp {
|
||||||
.night-mode .form-select:disabled {
|
.night-mode .form-select:disabled {
|
||||||
background-color: var(--fg-disabled);
|
background-color: var(--fg-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reduced-motion * {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
isCollapsed = true;
|
isCollapsed = true;
|
||||||
|
|
||||||
const height = collapse ? inner.clientHeight : getRequiredHeight(inner);
|
const height = collapse ? inner.clientHeight : getRequiredHeight(inner);
|
||||||
const duration = Math.sqrt(height * 80);
|
|
||||||
|
/* This function practically caps the maximum time at around 200ms,
|
||||||
|
but still allows to differentiate between small and large contents */
|
||||||
|
const duration = 10 + Math.pow(height, 1 / 4) * 20;
|
||||||
|
|
||||||
setStyle(height, duration);
|
setStyle(height, duration);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue