split out remaining rpc methods

@david-allison-1 note this also changes the method index to start at
0 instead of 1
This commit is contained in:
Damien Elmes 2021-03-11 16:53:36 +10:00
parent 378b116977
commit 4bd120cc4b
9 changed files with 331 additions and 319 deletions

View file

@ -137,11 +137,11 @@ def render_method(service_idx, method_idx, method):
{input_assign_outer}"""
if method.name in SKIP_DECODE:
buf += f"""return self._run_command({service_idx}, {method_idx+1}, input)
buf += f"""return self._run_command({service_idx}, {method_idx}, input)
"""
else:
buf += f"""output = pb.{method.output_type.name}()
output.ParseFromString(self._run_command({service_idx}, {method_idx+1}, input))
output.ParseFromString(self._run_command({service_idx}, {method_idx}, input))
return output{single_field}
"""

View file

@ -77,7 +77,7 @@ message DeckConfigID {
/// while the protobuf descriptors expose the order services are defined in,
/// that information is not available in prost, so we define an enum to make
/// sure all clients agree on the same service indices
/// sure all clients agree on the service index
enum ServiceIndex {
SERVICE_INDEX_SCHEDULING = 0;
SERVICE_INDEX_DECKS = 1;
@ -91,7 +91,9 @@ enum ServiceIndex {
SERVICE_INDEX_SEARCH = 9;
SERVICE_INDEX_STATS = 10;
SERVICE_INDEX_MEDIA = 11;
SERVICE_INDEX_BACKEND = 99;
SERVICE_INDEX_I18N = 12;
SERVICE_INDEX_COLLECTION = 13;
SERVICE_INDEX_CARDS = 14;
}
service SchedulingService {
@ -192,6 +194,7 @@ service CardRenderingService {
rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut);
rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut);
rpc StripAVTags(String) returns (String);
rpc RenderMarkdown(RenderMarkdownIn) returns (String);
}
service DeckConfigService {
@ -236,32 +239,28 @@ service MediaService {
rpc RestoreTrash(Empty) returns (Empty);
}
service BackendService {
rpc LatestProgress(Empty) returns (Progress);
rpc SetWantsAbort(Empty) returns (Empty);
// cards
rpc GetCard(CardID) returns (Card);
rpc UpdateCard(UpdateCardIn) returns (Empty);
rpc RemoveCards(RemoveCardsIn) returns (Empty);
rpc SetDeck(SetDeckIn) returns (Empty);
// collection
service I18nService {
rpc TranslateString(TranslateStringIn) returns (String);
rpc FormatTimespan(FormatTimespanIn) returns (String);
rpc I18nResources(Empty) returns (Json);
}
service CollectionService {
rpc OpenCollection(OpenCollectionIn) returns (Empty);
rpc CloseCollection(CloseCollectionIn) returns (Empty);
rpc CheckDatabase(Empty) returns (CheckDatabaseOut);
rpc GetUndoStatus(Empty) returns (UndoStatus);
rpc Undo(Empty) returns (UndoStatus);
rpc Redo(Empty) returns (UndoStatus);
rpc LatestProgress(Empty) returns (Progress);
rpc SetWantsAbort(Empty) returns (Empty);
}
// translation/messages/text manipulation
rpc TranslateString(TranslateStringIn) returns (String);
rpc FormatTimespan(FormatTimespanIn) returns (String);
rpc I18nResources(Empty) returns (Json);
rpc RenderMarkdown(RenderMarkdownIn) returns (String);
service CardsService {
rpc GetCard(CardID) returns (Card);
rpc UpdateCard(UpdateCardIn) returns (Empty);
rpc RemoveCards(RemoveCardsIn) returns (Empty);
rpc SetDeck(SetDeckIn) returns (Empty);
}
// Protobuf stored in .anki2 files

View file

@ -21,7 +21,7 @@ pub trait Service {
"{idx} => {{ let input = {input_type}::decode(input)?;\n",
"let output = self.{rust_method}(input)?;\n",
"let mut out_bytes = Vec::new(); output.encode(&mut out_bytes)?; Ok(out_bytes) }}, "),
idx = idx + 1,
idx = idx,
input_type = method.input_type,
rust_method = method.name
)

View file

@ -1,13 +1,60 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
use super::Backend;
use crate::prelude::*;
use crate::{
backend_proto as pb,
card::{CardQueue, CardType},
};
pub(super) use pb::cards_service::Service as CardsService;
impl CardsService for Backend {
fn get_card(&self, input: pb::CardId) -> Result<pb::Card> {
self.with_col(|col| {
col.storage
.get_card(input.into())
.and_then(|opt| opt.ok_or(AnkiError::NotFound))
.map(Into::into)
})
}
fn update_card(&self, input: pb::UpdateCardIn) -> Result<pb::Empty> {
self.with_col(|col| {
let op = if input.skip_undo_entry {
None
} else {
Some(UndoableOpKind::UpdateCard)
};
let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?;
col.update_card_with_op(&mut card, op)
})
.map(Into::into)
}
fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result<pb::Empty> {
self.with_col(|col| {
col.transact(None, |col| {
col.remove_cards_and_orphaned_notes(
&input
.card_ids
.into_iter()
.map(Into::into)
.collect::<Vec<_>>(),
)?;
Ok(().into())
})
})
}
fn set_deck(&self, input: pb::SetDeckIn) -> Result<pb::Empty> {
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
let deck_id = input.deck_id.into();
self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into))
}
}
impl TryFrom<pb::Card> for Card {
type Error = AnkiError;
@ -39,3 +86,28 @@ impl TryFrom<pb::Card> for Card {
})
}
}
impl From<Card> for pb::Card {
fn from(c: Card) -> Self {
pb::Card {
id: c.id.0,
note_id: c.note_id.0,
deck_id: c.deck_id.0,
template_idx: c.template_idx as u32,
mtime_secs: c.mtime.0,
usn: c.usn.0,
ctype: c.ctype as u32,
queue: c.queue as i32,
due: c.due,
interval: c.interval,
ease_factor: c.ease_factor as u32,
reps: c.reps,
lapses: c.lapses,
remaining_steps: c.remaining_steps,
original_due: c.original_due,
original_deck_id: c.original_deck_id.0,
flags: c.flags as u32,
data: c.data,
}
}
}

View file

@ -5,9 +5,11 @@ use super::Backend;
use crate::{
backend_proto as pb,
latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex},
notetype::CardTemplateSchema11,
markdown::render_markdown,
notetype::{CardTemplateSchema11, RenderCardOutput},
prelude::*,
text::{extract_av_tags, strip_av_tags, AVTag},
template::RenderedNode,
text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag},
};
pub(super) use pb::cardrendering_service::Service as CardRenderingService;
@ -116,4 +118,46 @@ impl CardRenderingService for Backend {
val: strip_av_tags(&input.val).into(),
})
}
fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result<pb::String> {
let mut text = render_markdown(&input.markdown);
if input.sanitize {
// currently no images
text = sanitize_html_no_images(&text);
}
Ok(text.into())
}
}
fn rendered_nodes_to_proto(nodes: Vec<RenderedNode>) -> Vec<pb::RenderedTemplateNode> {
nodes
.into_iter()
.map(|n| pb::RenderedTemplateNode {
value: Some(rendered_node_to_proto(n)),
})
.collect()
}
fn rendered_node_to_proto(node: RenderedNode) -> pb::rendered_template_node::Value {
match node {
RenderedNode::Text { text } => pb::rendered_template_node::Value::Text(text),
RenderedNode::Replacement {
field_name,
current_text,
filters,
} => pb::rendered_template_node::Value::Replacement(pb::RenderedTemplateReplacement {
field_name,
current_text,
filters,
}),
}
}
impl From<RenderCardOutput> for pb::RenderCardOut {
fn from(o: RenderCardOutput) -> Self {
pb::RenderCardOut {
question_nodes: rendered_nodes_to_proto(o.qnodes),
answer_nodes: rendered_nodes_to_proto(o.anodes),
}
}
}

View file

@ -0,0 +1,104 @@
// 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::progress::progress_to_proto,
backend_proto as pb,
collection::open_collection,
log::{self, default_logger},
prelude::*,
};
pub(super) use pb::collection_service::Service as CollectionService;
use slog::error;
impl CollectionService for Backend {
fn latest_progress(&self, _input: pb::Empty) -> Result<pb::Progress> {
let progress = self.progress_state.lock().unwrap().last_progress;
Ok(progress_to_proto(progress, &self.i18n))
}
fn set_wants_abort(&self, _input: pb::Empty) -> Result<pb::Empty> {
self.progress_state.lock().unwrap().want_abort = true;
Ok(().into())
}
fn open_collection(&self, input: pb::OpenCollectionIn) -> Result<pb::Empty> {
let mut col = self.col.lock().unwrap();
if col.is_some() {
return Err(AnkiError::CollectionAlreadyOpen);
}
let mut path = input.collection_path.clone();
path.push_str(".log");
let log_path = match input.log_path.as_str() {
"" => None,
path => Some(path),
};
let logger = default_logger(log_path)?;
let new_col = open_collection(
input.collection_path,
input.media_folder_path,
input.media_db_path,
self.server,
self.i18n.clone(),
logger,
)?;
*col = Some(new_col);
Ok(().into())
}
fn close_collection(&self, input: pb::CloseCollectionIn) -> Result<pb::Empty> {
self.abort_media_sync_and_wait();
let mut col = self.col.lock().unwrap();
if col.is_none() {
return Err(AnkiError::CollectionNotOpen);
}
let col_inner = col.take().unwrap();
if input.downgrade_to_schema11 {
let log = log::terminal();
if let Err(e) = col_inner.close(input.downgrade_to_schema11) {
error!(log, " failed: {:?}", e);
}
}
Ok(().into())
}
fn check_database(&self, _input: pb::Empty) -> Result<pb::CheckDatabaseOut> {
let mut handler = self.new_progress_handler();
let progress_fn = move |progress, throttle| {
handler.update(Progress::DatabaseCheck(progress), throttle);
};
self.with_col(|col| {
col.check_database(progress_fn)
.map(|problems| pb::CheckDatabaseOut {
problems: problems.to_i18n_strings(&col.i18n),
})
})
}
fn get_undo_status(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| Ok(col.undo_status()))
}
fn undo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| {
col.undo()?;
Ok(col.undo_status())
})
}
fn redo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| {
col.redo()?;
Ok(col.undo_status())
})
}
}

55
rslib/src/backend/i18n.rs Normal file
View file

@ -0,0 +1,55 @@
// 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::*,
scheduler::timespan::{answer_button_time, time_span},
};
use fluent::FluentValue;
pub(super) use pb::i18n_service::Service as I18nService;
impl I18nService for Backend {
fn translate_string(&self, input: pb::TranslateStringIn) -> Result<pb::String> {
let key = match crate::fluent_proto::FluentString::from_i32(input.key) {
Some(key) => key,
None => return Ok("invalid key".to_string().into()),
};
let map = input
.args
.iter()
.map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v)))
.collect();
Ok(self.i18n.trn(key, map).into())
}
fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result<pb::String> {
use pb::format_timespan_in::Context;
Ok(match input.context() {
Context::Precise => time_span(input.seconds, &self.i18n, true),
Context::Intervals => time_span(input.seconds, &self.i18n, false),
Context::AnswerButtons => answer_button_time(input.seconds, &self.i18n),
}
.into())
}
fn i18n_resources(&self, _input: pb::Empty) -> Result<pb::Json> {
serde_json::to_vec(&self.i18n.resources_for_js())
.map(Into::into)
.map_err(Into::into)
}
}
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
use pb::translate_arg_value::Value as V;
match &arg.value {
Some(val) => match val {
V::Str(s) => FluentValue::String(s.into()),
V::Number(f) => FluentValue::Number(f.into()),
},
None => FluentValue::String("".into()),
}
}

View file

@ -4,12 +4,14 @@
mod adding;
mod card;
mod cardrendering;
mod collection;
mod config;
mod dbproxy;
mod deckconfig;
mod decks;
mod err;
mod generic;
mod i18n;
mod media;
mod notes;
mod notetypes;
@ -21,54 +23,42 @@ mod sync;
mod tags;
use self::{
card::CardsService,
cardrendering::CardRenderingService,
collection::CollectionService,
config::ConfigService,
deckconfig::DeckConfigService,
decks::DecksService,
i18n::I18nService,
media::MediaService,
notes::NotesService,
notetypes::NoteTypesService,
progress::ProgressState,
scheduler::SchedulingService,
search::SearchService,
stats::StatsService,
sync::{SyncService, SyncState},
tags::TagsService,
};
use crate::backend_proto::backend_service::Service as BackendService;
use crate::{
backend::dbproxy::db_command_bytes,
backend_proto as pb,
backend_proto::RenderedTemplateReplacement,
card::{Card, CardID},
collection::{open_collection, Collection},
collection::Collection,
err::{AnkiError, Result},
i18n::I18n,
log,
log::default_logger,
markdown::render_markdown,
notetype::RenderCardOutput,
scheduler::timespan::{answer_button_time, time_span},
template::RenderedNode,
text::sanitize_html_no_images,
undo::UndoableOpKind,
};
use fluent::FluentValue;
use log::error;
use once_cell::sync::OnceCell;
use progress::{AbortHandleSlot, Progress};
use progress::AbortHandleSlot;
use prost::Message;
use std::convert::TryInto;
use std::{
result,
sync::{Arc, Mutex},
};
use tokio::runtime::{self, Runtime};
use self::{
err::anki_error_to_proto_error,
progress::{progress_to_proto, ProgressState},
};
use self::err::anki_error_to_proto_error;
pub struct Backend {
col: Arc<Mutex<Option<Collection>>>,
@ -80,8 +70,6 @@ pub struct Backend {
state: Arc<Mutex<BackendState>>,
}
// fixme: move other items like runtime into here as well
#[derive(Default)]
struct BackendState {
sync: SyncState,
@ -102,189 +90,6 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result<Backend, String> {
Ok(Backend::new(i18n, input.server))
}
impl BackendService for Backend {
fn latest_progress(&self, _input: pb::Empty) -> Result<pb::Progress> {
let progress = self.progress_state.lock().unwrap().last_progress;
Ok(progress_to_proto(progress, &self.i18n))
}
fn set_wants_abort(&self, _input: pb::Empty) -> Result<pb::Empty> {
self.progress_state.lock().unwrap().want_abort = true;
Ok(().into())
}
// cards
//-------------------------------------------------------------------
fn get_card(&self, input: pb::CardId) -> Result<pb::Card> {
self.with_col(|col| {
col.storage
.get_card(input.into())
.and_then(|opt| opt.ok_or(AnkiError::NotFound))
.map(Into::into)
})
}
fn update_card(&self, input: pb::UpdateCardIn) -> Result<pb::Empty> {
self.with_col(|col| {
let op = if input.skip_undo_entry {
None
} else {
Some(UndoableOpKind::UpdateCard)
};
let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?;
col.update_card_with_op(&mut card, op)
})
.map(Into::into)
}
fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result<pb::Empty> {
self.with_col(|col| {
col.transact(None, |col| {
col.remove_cards_and_orphaned_notes(
&input
.card_ids
.into_iter()
.map(Into::into)
.collect::<Vec<_>>(),
)?;
Ok(().into())
})
})
}
fn set_deck(&self, input: pb::SetDeckIn) -> Result<pb::Empty> {
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
let deck_id = input.deck_id.into();
self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into))
}
// collection
//-------------------------------------------------------------------
fn open_collection(&self, input: pb::OpenCollectionIn) -> Result<pb::Empty> {
let mut col = self.col.lock().unwrap();
if col.is_some() {
return Err(AnkiError::CollectionAlreadyOpen);
}
let mut path = input.collection_path.clone();
path.push_str(".log");
let log_path = match input.log_path.as_str() {
"" => None,
path => Some(path),
};
let logger = default_logger(log_path)?;
let new_col = open_collection(
input.collection_path,
input.media_folder_path,
input.media_db_path,
self.server,
self.i18n.clone(),
logger,
)?;
*col = Some(new_col);
Ok(().into())
}
fn close_collection(&self, input: pb::CloseCollectionIn) -> Result<pb::Empty> {
self.abort_media_sync_and_wait();
let mut col = self.col.lock().unwrap();
if col.is_none() {
return Err(AnkiError::CollectionNotOpen);
}
let col_inner = col.take().unwrap();
if input.downgrade_to_schema11 {
let log = log::terminal();
if let Err(e) = col_inner.close(input.downgrade_to_schema11) {
error!(log, " failed: {:?}", e);
}
}
Ok(().into())
}
fn check_database(&self, _input: pb::Empty) -> Result<pb::CheckDatabaseOut> {
let mut handler = self.new_progress_handler();
let progress_fn = move |progress, throttle| {
handler.update(Progress::DatabaseCheck(progress), throttle);
};
self.with_col(|col| {
col.check_database(progress_fn)
.map(|problems| pb::CheckDatabaseOut {
problems: problems.to_i18n_strings(&col.i18n),
})
})
}
fn get_undo_status(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| Ok(col.undo_status()))
}
fn undo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| {
col.undo()?;
Ok(col.undo_status())
})
}
fn redo(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| {
col.redo()?;
Ok(col.undo_status())
})
}
// i18n/messages
//-------------------------------------------------------------------
fn translate_string(&self, input: pb::TranslateStringIn) -> Result<pb::String> {
let key = match crate::fluent_proto::FluentString::from_i32(input.key) {
Some(key) => key,
None => return Ok("invalid key".to_string().into()),
};
let map = input
.args
.iter()
.map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v)))
.collect();
Ok(self.i18n.trn(key, map).into())
}
fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result<pb::String> {
use pb::format_timespan_in::Context;
Ok(match input.context() {
Context::Precise => time_span(input.seconds, &self.i18n, true),
Context::Intervals => time_span(input.seconds, &self.i18n, false),
Context::AnswerButtons => answer_button_time(input.seconds, &self.i18n),
}
.into())
}
fn i18n_resources(&self, _input: pb::Empty) -> Result<pb::Json> {
serde_json::to_vec(&self.i18n.resources_for_js())
.map(Into::into)
.map_err(Into::into)
}
fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result<pb::String> {
let mut text = render_markdown(&input.markdown);
if input.sanitize {
// currently no images
text = sanitize_html_no_images(&text);
}
Ok(text.into())
}
}
impl Backend {
pub fn new(i18n: I18n, server: bool) -> Backend {
Backend {
@ -315,7 +120,6 @@ impl Backend {
.ok_or_else(|| AnkiError::invalid_input("invalid service"))
.and_then(|service| match service {
pb::ServiceIndex::Scheduling => SchedulingService::run_method(self, method, input),
pb::ServiceIndex::Backend => BackendService::run_method(self, method, input),
pb::ServiceIndex::Decks => DecksService::run_method(self, method, input),
pb::ServiceIndex::Notes => NotesService::run_method(self, method, input),
pb::ServiceIndex::NoteTypes => NoteTypesService::run_method(self, method, input),
@ -329,6 +133,9 @@ impl Backend {
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),
pb::ServiceIndex::I18n => I18nService::run_method(self, method, input),
pb::ServiceIndex::Collection => CollectionService::run_method(self, method, input),
pb::ServiceIndex::Cards => CardsService::run_method(self, method, input),
})
.map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.i18n);
@ -338,6 +145,15 @@ impl Backend {
})
}
pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> {
self.db_command(input).map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.i18n);
let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap();
bytes
})
}
/// If collection is open, run the provided closure while holding
/// the mutex.
/// If collection is not open, return an error.
@ -368,94 +184,7 @@ impl Backend {
.clone()
}
pub fn db_command(&self, input: &[u8]) -> Result<Vec<u8>> {
fn db_command(&self, input: &[u8]) -> Result<Vec<u8>> {
self.with_col(|col| db_command_bytes(col, input))
}
pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result<Vec<u8>, Vec<u8>> {
self.db_command(input).map_err(|err| {
let backend_err = anki_error_to_proto_error(err, &self.i18n);
let mut bytes = Vec::new();
backend_err.encode(&mut bytes).unwrap();
bytes
})
}
}
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
use pb::translate_arg_value::Value as V;
match &arg.value {
Some(val) => match val {
V::Str(s) => FluentValue::String(s.into()),
V::Number(f) => FluentValue::Number(f.into()),
},
None => FluentValue::String("".into()),
}
}
fn rendered_nodes_to_proto(nodes: Vec<RenderedNode>) -> Vec<pb::RenderedTemplateNode> {
nodes
.into_iter()
.map(|n| pb::RenderedTemplateNode {
value: Some(rendered_node_to_proto(n)),
})
.collect()
}
fn rendered_node_to_proto(node: RenderedNode) -> pb::rendered_template_node::Value {
match node {
RenderedNode::Text { text } => pb::rendered_template_node::Value::Text(text),
RenderedNode::Replacement {
field_name,
current_text,
filters,
} => pb::rendered_template_node::Value::Replacement(RenderedTemplateReplacement {
field_name,
current_text,
filters,
}),
}
}
impl From<RenderCardOutput> for pb::RenderCardOut {
fn from(o: RenderCardOutput) -> Self {
pb::RenderCardOut {
question_nodes: rendered_nodes_to_proto(o.qnodes),
answer_nodes: rendered_nodes_to_proto(o.anodes),
}
}
}
impl From<Card> for pb::Card {
fn from(c: Card) -> Self {
pb::Card {
id: c.id.0,
note_id: c.note_id.0,
deck_id: c.deck_id.0,
template_idx: c.template_idx as u32,
mtime_secs: c.mtime.0,
usn: c.usn.0,
ctype: c.ctype as u32,
queue: c.queue as i32,
due: c.due,
interval: c.interval,
ease_factor: c.ease_factor as u32,
reps: c.reps,
lapses: c.lapses,
remaining_steps: c.remaining_steps,
original_due: c.original_due,
original_deck_id: c.original_deck_id.0,
flags: c.flags as u32,
data: c.data,
}
}
}
impl From<crate::scheduler::timing::SchedTimingToday> for pb::SchedTimingTodayOut {
fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut {
pb::SchedTimingTodayOut {
days_elapsed: t.days_elapsed,
next_day_at: t.next_day_at,
}
}
}

View file

@ -175,3 +175,12 @@ impl SchedulingService for Backend {
self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only))
}
}
impl From<crate::scheduler::timing::SchedTimingToday> for pb::SchedTimingTodayOut {
fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut {
pb::SchedTimingTodayOut {
days_elapsed: t.days_elapsed,
next_day_at: t.next_day_at,
}
}
}