mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 21:36:55 -05:00
split out stats, media and search
This commit is contained in:
parent
cd14987812
commit
378b116977
5 changed files with 211 additions and 185 deletions
|
|
@ -88,6 +88,9 @@ enum ServiceIndex {
|
||||||
SERVICE_INDEX_CARD_RENDERING = 6;
|
SERVICE_INDEX_CARD_RENDERING = 6;
|
||||||
SERVICE_INDEX_DECK_CONFIG = 7;
|
SERVICE_INDEX_DECK_CONFIG = 7;
|
||||||
SERVICE_INDEX_TAGS = 8;
|
SERVICE_INDEX_TAGS = 8;
|
||||||
|
SERVICE_INDEX_SEARCH = 9;
|
||||||
|
SERVICE_INDEX_STATS = 10;
|
||||||
|
SERVICE_INDEX_MEDIA = 11;
|
||||||
SERVICE_INDEX_BACKEND = 99;
|
SERVICE_INDEX_BACKEND = 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,33 +212,33 @@ service TagsService {
|
||||||
rpc DragDropTags(DragDropTagsIn) returns (Empty);
|
rpc DragDropTags(DragDropTagsIn) returns (Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
service BackendService {
|
service SearchService {
|
||||||
rpc LatestProgress(Empty) returns (Progress);
|
|
||||||
rpc SetWantsAbort(Empty) returns (Empty);
|
|
||||||
|
|
||||||
// searching
|
|
||||||
|
|
||||||
rpc BuildSearchString(SearchNode) returns (String);
|
rpc BuildSearchString(SearchNode) returns (String);
|
||||||
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
||||||
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
||||||
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
|
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
|
||||||
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
|
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
|
||||||
rpc FindAndReplace(FindAndReplaceIn) returns (UInt32);
|
rpc FindAndReplace(FindAndReplaceIn) returns (UInt32);
|
||||||
|
}
|
||||||
|
|
||||||
// stats
|
service StatsService {
|
||||||
|
|
||||||
rpc CardStats(CardID) returns (String);
|
rpc CardStats(CardID) returns (String);
|
||||||
rpc Graphs(GraphsIn) returns (GraphsOut);
|
rpc Graphs(GraphsIn) returns (GraphsOut);
|
||||||
rpc GetGraphPreferences(Empty) returns (GraphPreferences);
|
rpc GetGraphPreferences(Empty) returns (GraphPreferences);
|
||||||
rpc SetGraphPreferences(GraphPreferences) returns (Empty);
|
rpc SetGraphPreferences(GraphPreferences) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
// media
|
service MediaService {
|
||||||
|
|
||||||
rpc CheckMedia(Empty) returns (CheckMediaOut);
|
rpc CheckMedia(Empty) returns (CheckMediaOut);
|
||||||
rpc TrashMediaFiles(TrashMediaFilesIn) returns (Empty);
|
rpc TrashMediaFiles(TrashMediaFilesIn) returns (Empty);
|
||||||
rpc AddMediaFile(AddMediaFileIn) returns (String);
|
rpc AddMediaFile(AddMediaFileIn) returns (String);
|
||||||
rpc EmptyTrash(Empty) returns (Empty);
|
rpc EmptyTrash(Empty) returns (Empty);
|
||||||
rpc RestoreTrash(Empty) returns (Empty);
|
rpc RestoreTrash(Empty) returns (Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
service BackendService {
|
||||||
|
rpc LatestProgress(Empty) returns (Progress);
|
||||||
|
rpc SetWantsAbort(Empty) returns (Empty);
|
||||||
|
|
||||||
// cards
|
// cards
|
||||||
|
|
||||||
|
|
|
||||||
89
rslib/src/backend/media.rs
Normal file
89
rslib/src/backend/media.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::{progress::Progress, Backend};
|
||||||
|
use crate::{
|
||||||
|
backend_proto as pb,
|
||||||
|
media::{check::MediaChecker, MediaManager},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
pub(super) use pb::media_service::Service as MediaService;
|
||||||
|
|
||||||
|
impl MediaService for Backend {
|
||||||
|
// media
|
||||||
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
fn check_media(&self, _input: pb::Empty) -> Result<pb::CheckMediaOut> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
let mut output = checker.check()?;
|
||||||
|
|
||||||
|
let report = checker.summarize_output(&mut output);
|
||||||
|
|
||||||
|
Ok(pb::CheckMediaOut {
|
||||||
|
unused: output.unused,
|
||||||
|
missing: output.missing,
|
||||||
|
report,
|
||||||
|
have_trash: output.trash_count > 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result<pb::Empty> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
let mut ctx = mgr.dbctx();
|
||||||
|
mgr.remove_files(&mut ctx, &input.fnames)
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result<pb::String> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
let mut ctx = mgr.dbctx();
|
||||||
|
Ok(mgr
|
||||||
|
.add_file(&mut ctx, &input.desired_name, &input.data)?
|
||||||
|
.to_string()
|
||||||
|
.into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_trash(&self, _input: pb::Empty) -> Result<pb::Empty> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
|
||||||
|
checker.empty_trash()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_trash(&self, _input: pb::Empty) -> Result<pb::Empty> {
|
||||||
|
let mut handler = self.new_progress_handler();
|
||||||
|
let progress_fn =
|
||||||
|
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
||||||
|
|
||||||
|
col.transact(None, |ctx| {
|
||||||
|
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
||||||
|
|
||||||
|
checker.restore_trash()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,13 @@ mod deckconfig;
|
||||||
mod decks;
|
mod decks;
|
||||||
mod err;
|
mod err;
|
||||||
mod generic;
|
mod generic;
|
||||||
|
mod media;
|
||||||
mod notes;
|
mod notes;
|
||||||
mod notetypes;
|
mod notetypes;
|
||||||
mod progress;
|
mod progress;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod search;
|
mod search;
|
||||||
|
mod stats;
|
||||||
mod sync;
|
mod sync;
|
||||||
mod tags;
|
mod tags;
|
||||||
|
|
||||||
|
|
@ -23,9 +25,12 @@ use self::{
|
||||||
config::ConfigService,
|
config::ConfigService,
|
||||||
deckconfig::DeckConfigService,
|
deckconfig::DeckConfigService,
|
||||||
decks::DecksService,
|
decks::DecksService,
|
||||||
|
media::MediaService,
|
||||||
notes::NotesService,
|
notes::NotesService,
|
||||||
notetypes::NoteTypesService,
|
notetypes::NoteTypesService,
|
||||||
scheduler::SchedulingService,
|
scheduler::SchedulingService,
|
||||||
|
search::SearchService,
|
||||||
|
stats::StatsService,
|
||||||
sync::{SyncService, SyncState},
|
sync::{SyncService, SyncState},
|
||||||
tags::TagsService,
|
tags::TagsService,
|
||||||
};
|
};
|
||||||
|
|
@ -42,12 +47,8 @@ use crate::{
|
||||||
log,
|
log,
|
||||||
log::default_logger,
|
log::default_logger,
|
||||||
markdown::render_markdown,
|
markdown::render_markdown,
|
||||||
media::check::MediaChecker,
|
|
||||||
media::MediaManager,
|
|
||||||
notes::NoteID,
|
|
||||||
notetype::RenderCardOutput,
|
notetype::RenderCardOutput,
|
||||||
scheduler::timespan::{answer_button_time, time_span},
|
scheduler::timespan::{answer_button_time, time_span},
|
||||||
search::{concatenate_searches, replace_search_node, write_nodes, Node},
|
|
||||||
template::RenderedNode,
|
template::RenderedNode,
|
||||||
text::sanitize_html_no_images,
|
text::sanitize_html_no_images,
|
||||||
undo::UndoableOpKind,
|
undo::UndoableOpKind,
|
||||||
|
|
@ -112,175 +113,6 @@ impl BackendService for Backend {
|
||||||
Ok(().into())
|
Ok(().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
// searching
|
|
||||||
//-----------------------------------------------
|
|
||||||
|
|
||||||
fn build_search_string(&self, input: pb::SearchNode) -> Result<pb::String> {
|
|
||||||
let node: Node = input.try_into()?;
|
|
||||||
Ok(write_nodes(&node.into_node_list()).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_cards(&self, input: pb::SearchCardsIn) -> Result<pb::SearchCardsOut> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let order = input.order.unwrap_or_default().value.into();
|
|
||||||
let cids = col.search_cards(&input.search, order)?;
|
|
||||||
Ok(pb::SearchCardsOut {
|
|
||||||
card_ids: cids.into_iter().map(|v| v.0).collect(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_notes(&self, input: pb::SearchNotesIn) -> Result<pb::SearchNotesOut> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let nids = col.search_notes(&input.search)?;
|
|
||||||
Ok(pb::SearchNotesOut {
|
|
||||||
note_ids: nids.into_iter().map(|v| v.0).collect(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result<pb::String> {
|
|
||||||
let sep = input.joiner().into();
|
|
||||||
let existing_nodes = {
|
|
||||||
let node: Node = input.existing_node.unwrap_or_default().try_into()?;
|
|
||||||
node.into_node_list()
|
|
||||||
};
|
|
||||||
let additional_node = input.additional_node.unwrap_or_default().try_into()?;
|
|
||||||
Ok(concatenate_searches(sep, existing_nodes, additional_node).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result<pb::String> {
|
|
||||||
let existing = {
|
|
||||||
let node = input.existing_node.unwrap_or_default().try_into()?;
|
|
||||||
if let Node::Group(nodes) = node {
|
|
||||||
nodes
|
|
||||||
} else {
|
|
||||||
vec![node]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let replacement = input.replacement_node.unwrap_or_default().try_into()?;
|
|
||||||
Ok(replace_search_node(existing, replacement).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result<pb::UInt32> {
|
|
||||||
let mut search = if input.regex {
|
|
||||||
input.search
|
|
||||||
} else {
|
|
||||||
regex::escape(&input.search)
|
|
||||||
};
|
|
||||||
if !input.match_case {
|
|
||||||
search = format!("(?i){}", search);
|
|
||||||
}
|
|
||||||
let nids = input.nids.into_iter().map(NoteID).collect();
|
|
||||||
let field_name = if input.field_name.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(input.field_name)
|
|
||||||
};
|
|
||||||
let repl = input.replacement;
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.find_and_replace(nids, &search, &repl, field_name)
|
|
||||||
.map(|cnt| pb::UInt32 { val: cnt as u32 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// statistics
|
|
||||||
//-----------------------------------------------
|
|
||||||
|
|
||||||
fn card_stats(&self, input: pb::CardId) -> Result<pb::String> {
|
|
||||||
self.with_col(|col| col.card_stats(input.into()))
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn graphs(&self, input: pb::GraphsIn) -> Result<pb::GraphsOut> {
|
|
||||||
self.with_col(|col| col.graph_data_for_search(&input.search, input.days))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_graph_preferences(&self, _input: pb::Empty) -> Result<pb::GraphPreferences> {
|
|
||||||
self.with_col(|col| col.get_graph_preferences())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result<pb::Empty> {
|
|
||||||
self.with_col(|col| col.set_graph_preferences(input))
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
// media
|
|
||||||
//-----------------------------------------------
|
|
||||||
|
|
||||||
fn check_media(&self, _input: pb::Empty) -> Result<pb::CheckMediaOut> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
let mut output = checker.check()?;
|
|
||||||
|
|
||||||
let report = checker.summarize_output(&mut output);
|
|
||||||
|
|
||||||
Ok(pb::CheckMediaOut {
|
|
||||||
unused: output.unused,
|
|
||||||
missing: output.missing,
|
|
||||||
report,
|
|
||||||
have_trash: output.trash_count > 0,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result<pb::Empty> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
let mut ctx = mgr.dbctx();
|
|
||||||
mgr.remove_files(&mut ctx, &input.fnames)
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result<pb::String> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
let mut ctx = mgr.dbctx();
|
|
||||||
Ok(mgr
|
|
||||||
.add_file(&mut ctx, &input.desired_name, &input.data)?
|
|
||||||
.to_string()
|
|
||||||
.into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn empty_trash(&self, _input: pb::Empty) -> Result<pb::Empty> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
|
|
||||||
checker.empty_trash()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_trash(&self, _input: pb::Empty) -> Result<pb::Empty> {
|
|
||||||
let mut handler = self.new_progress_handler();
|
|
||||||
let progress_fn =
|
|
||||||
move |progress| handler.update(Progress::MediaCheck(progress as u32), true);
|
|
||||||
self.with_col(|col| {
|
|
||||||
let mgr = MediaManager::new(&col.media_folder, &col.media_db)?;
|
|
||||||
|
|
||||||
col.transact(None, |ctx| {
|
|
||||||
let mut checker = MediaChecker::new(ctx, &mgr, progress_fn);
|
|
||||||
|
|
||||||
checker.restore_trash()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cards
|
// cards
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -494,6 +326,9 @@ impl Backend {
|
||||||
pb::ServiceIndex::CardRendering => {
|
pb::ServiceIndex::CardRendering => {
|
||||||
CardRenderingService::run_method(self, method, input)
|
CardRenderingService::run_method(self, method, input)
|
||||||
}
|
}
|
||||||
|
pb::ServiceIndex::Media => MediaService::run_method(self, method, input),
|
||||||
|
pb::ServiceIndex::Stats => StatsService::run_method(self, method, input),
|
||||||
|
pb::ServiceIndex::Search => SearchService::run_method(self, method, input),
|
||||||
})
|
})
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
let backend_err = anki_error_to_proto_error(err, &self.i18n);
|
let backend_err = anki_error_to_proto_error(err, &self.i18n);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
|
use super::Backend;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto as pb,
|
backend_proto as pb,
|
||||||
backend_proto::{
|
backend_proto::{
|
||||||
|
|
@ -12,11 +13,83 @@ use crate::{
|
||||||
config::SortKind,
|
config::SortKind,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
search::{
|
search::{
|
||||||
parse_search, BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode,
|
concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node,
|
||||||
StateKind, TemplateKind,
|
PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind,
|
||||||
},
|
},
|
||||||
text::escape_anki_wildcards,
|
text::escape_anki_wildcards,
|
||||||
};
|
};
|
||||||
|
pub(super) use pb::search_service::Service as SearchService;
|
||||||
|
|
||||||
|
impl SearchService for Backend {
|
||||||
|
fn build_search_string(&self, input: pb::SearchNode) -> Result<pb::String> {
|
||||||
|
let node: Node = input.try_into()?;
|
||||||
|
Ok(write_nodes(&node.into_node_list()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_cards(&self, input: pb::SearchCardsIn) -> Result<pb::SearchCardsOut> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let order = input.order.unwrap_or_default().value.into();
|
||||||
|
let cids = col.search_cards(&input.search, order)?;
|
||||||
|
Ok(pb::SearchCardsOut {
|
||||||
|
card_ids: cids.into_iter().map(|v| v.0).collect(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_notes(&self, input: pb::SearchNotesIn) -> Result<pb::SearchNotesOut> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let nids = col.search_notes(&input.search)?;
|
||||||
|
Ok(pb::SearchNotesOut {
|
||||||
|
note_ids: nids.into_iter().map(|v| v.0).collect(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result<pb::String> {
|
||||||
|
let sep = input.joiner().into();
|
||||||
|
let existing_nodes = {
|
||||||
|
let node: Node = input.existing_node.unwrap_or_default().try_into()?;
|
||||||
|
node.into_node_list()
|
||||||
|
};
|
||||||
|
let additional_node = input.additional_node.unwrap_or_default().try_into()?;
|
||||||
|
Ok(concatenate_searches(sep, existing_nodes, additional_node).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result<pb::String> {
|
||||||
|
let existing = {
|
||||||
|
let node = input.existing_node.unwrap_or_default().try_into()?;
|
||||||
|
if let Node::Group(nodes) = node {
|
||||||
|
nodes
|
||||||
|
} else {
|
||||||
|
vec![node]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let replacement = input.replacement_node.unwrap_or_default().try_into()?;
|
||||||
|
Ok(replace_search_node(existing, replacement).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result<pb::UInt32> {
|
||||||
|
let mut search = if input.regex {
|
||||||
|
input.search
|
||||||
|
} else {
|
||||||
|
regex::escape(&input.search)
|
||||||
|
};
|
||||||
|
if !input.match_case {
|
||||||
|
search = format!("(?i){}", search);
|
||||||
|
}
|
||||||
|
let nids = input.nids.into_iter().map(NoteID).collect();
|
||||||
|
let field_name = if input.field_name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(input.field_name)
|
||||||
|
};
|
||||||
|
let repl = input.replacement;
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.find_and_replace(nids, &search, &repl, field_name)
|
||||||
|
.map(|cnt| pb::UInt32 { val: cnt as u32 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<pb::SearchNode> for Node {
|
impl TryFrom<pb::SearchNode> for Node {
|
||||||
type Error = AnkiError;
|
type Error = AnkiError;
|
||||||
|
|
|
||||||
26
rslib/src/backend/stats.rs
Normal file
26
rslib/src/backend/stats.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::Backend;
|
||||||
|
use crate::{backend_proto as pb, prelude::*};
|
||||||
|
pub(super) use pb::stats_service::Service as StatsService;
|
||||||
|
|
||||||
|
impl StatsService for Backend {
|
||||||
|
fn card_stats(&self, input: pb::CardId) -> Result<pb::String> {
|
||||||
|
self.with_col(|col| col.card_stats(input.into()))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graphs(&self, input: pb::GraphsIn) -> Result<pb::GraphsOut> {
|
||||||
|
self.with_col(|col| col.graph_data_for_search(&input.search, input.days))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_graph_preferences(&self, _input: pb::Empty) -> Result<pb::GraphPreferences> {
|
||||||
|
self.with_col(|col| col.get_graph_preferences())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result<pb::Empty> {
|
||||||
|
self.with_col(|col| col.set_graph_preferences(input))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue