mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Enable removal of multiple tags from the sidebar
This commit is contained in:
parent
f4aeb0c097
commit
25d57574c9
7 changed files with 75 additions and 7 deletions
|
@ -349,7 +349,7 @@ class SidebarTreeView(QTreeView):
|
|||
),
|
||||
SidebarItemType.TAG: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_tag),
|
||||
(tr(TR.ACTIONS_DELETE), self.remove_tag),
|
||||
(tr(TR.ACTIONS_DELETE), self.remove_tags),
|
||||
),
|
||||
SidebarItemType.SAVED_SEARCH: (
|
||||
(tr(TR.ACTIONS_RENAME), self.rename_saved_search),
|
||||
|
@ -1081,15 +1081,14 @@ class SidebarTreeView(QTreeView):
|
|||
self.refresh()
|
||||
self.mw.deckBrowser.refresh()
|
||||
|
||||
def remove_tag(self, item: SidebarItem) -> None:
|
||||
self.browser.editor.saveNow(lambda: self._remove_tag(item))
|
||||
def remove_tags(self, item: SidebarItem) -> None:
|
||||
self.browser.editor.saveNow(lambda: self._remove_tags(item))
|
||||
|
||||
def _remove_tag(self, item: SidebarItem) -> None:
|
||||
old_name = item.full_name
|
||||
def _remove_tags(self, _item: SidebarItem) -> None:
|
||||
tags = self._selected_tags()
|
||||
|
||||
def do_remove() -> None:
|
||||
self.mw.col.tags.remove(old_name)
|
||||
self.col.tags.rename(old_name, "")
|
||||
self.col._backend.expunge_tags(" ".join(tags))
|
||||
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
||||
|
@ -1246,3 +1245,10 @@ class SidebarTreeView(QTreeView):
|
|||
for item in self._selected_items()
|
||||
if item.item_type == SidebarItemType.SAVED_SEARCH
|
||||
]
|
||||
|
||||
def _selected_tags(self) -> List[str]:
|
||||
return [
|
||||
item.full_name
|
||||
for item in self._selected_items()
|
||||
if item.item_type == SidebarItemType.TAG
|
||||
]
|
||||
|
|
|
@ -225,6 +225,7 @@ service BackendService {
|
|||
|
||||
rpc ClearUnusedTags(Empty) returns (Empty);
|
||||
rpc AllTags(Empty) returns (StringList);
|
||||
rpc ExpungeTags(String) returns (Empty);
|
||||
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
||||
rpc ClearTag(String) returns (Empty);
|
||||
rpc TagTree(Empty) returns (TagTreeNode);
|
||||
|
|
|
@ -1418,6 +1418,13 @@ impl BackendService for Backend {
|
|||
})
|
||||
}
|
||||
|
||||
fn expunge_tags(&self, tags: pb::String) -> BackendResult<pb::Empty> {
|
||||
self.with_col(|col| {
|
||||
col.expunge_tags(tags.val.as_str())?;
|
||||
Ok(().into())
|
||||
})
|
||||
}
|
||||
|
||||
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult<pb::Empty> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
|
|
|
@ -487,3 +487,11 @@ impl From<ParseIntError> for AnkiError {
|
|||
AnkiError::ParseNumError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<regex::Error> for AnkiError {
|
||||
fn from(_err: regex::Error) -> Self {
|
||||
AnkiError::InvalidInput {
|
||||
info: "invalid regex".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,6 +152,12 @@ impl Note {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_tags(&mut self, re: &Regex) -> bool {
|
||||
let old_len = self.tags.len();
|
||||
self.tags.retain(|tag| !re.is_match(tag));
|
||||
old_len > self.tags.len()
|
||||
}
|
||||
|
||||
pub(crate) fn replace_tags<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
|
||||
let mut changed = false;
|
||||
for tag in &mut self.tags {
|
||||
|
|
|
@ -73,6 +73,15 @@ impl SqliteStorage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all matching tags where tag_group is a regexp group that should not match whitespace.
|
||||
pub(crate) fn clear_tag_group(&self, tag_group: &str) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("delete from tags where tag regexp ?")?
|
||||
.execute(&[format!("(?i)^{}($|::)", tag_group)])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
||||
|
|
|
@ -285,6 +285,37 @@ impl Collection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Take tags as a whitespace-separated string and remove them from all notes and the storage.
|
||||
pub fn expunge_tags(&mut self, tags: &str) -> Result<usize> {
|
||||
let tag_group = format!("({})", regex::escape(tags.trim()).replace(' ', "|"));
|
||||
let nids = self.nids_for_tags(&tag_group)?;
|
||||
let re = Regex::new(&format!("(?i)^{}(::.*)?$", tag_group))?;
|
||||
self.transact(None, |col| {
|
||||
col.storage.clear_tag_group(&tag_group)?;
|
||||
col.transform_notes(&nids, |note, _nt| {
|
||||
Ok(TransformNoteOutput {
|
||||
changed: note.remove_tags(&re),
|
||||
generate_cards: false,
|
||||
mark_modified: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Take tags as a regexp group, i.e. separated with pipes and wrapped in brackets, and return
|
||||
/// the ids of all notes with one of them.
|
||||
fn nids_for_tags(&mut self, tag_group: &str) -> Result<Vec<NoteID>> {
|
||||
let mut stmt = self
|
||||
.storage
|
||||
.db
|
||||
.prepare("select id from notes where tags regexp ?")?;
|
||||
let args = format!("(?i).* {}(::| ).*", tag_group);
|
||||
let nids = stmt
|
||||
.query_map(&[args], |row| row.get(0))?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
Ok(nids)
|
||||
}
|
||||
|
||||
pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> {
|
||||
let mut name = name;
|
||||
let tag;
|
||||
|
|
Loading…
Reference in a new issue