mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
log manual reschedule, but ignore the log entry in the stats
This commit is contained in:
parent
39212a38aa
commit
ce49ca9401
21 changed files with 249 additions and 82 deletions
|
@ -95,7 +95,8 @@ service BackendService {
|
|||
rpc LocalMinutesWest (Int64) returns (Int32);
|
||||
rpc SetLocalMinutesWest (Int32) returns (Empty);
|
||||
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
|
||||
rpc StudiedToday (StudiedTodayIn) returns (String);
|
||||
rpc StudiedToday (Empty) returns (String);
|
||||
rpc StudiedTodayMessage (StudiedTodayMessageIn) returns (String);
|
||||
rpc UpdateStats (UpdateStatsIn) returns (Empty);
|
||||
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
|
||||
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
|
||||
|
@ -685,7 +686,7 @@ message FormatTimespanIn {
|
|||
Context context = 2;
|
||||
}
|
||||
|
||||
message StudiedTodayIn {
|
||||
message StudiedTodayMessageIn {
|
||||
uint32 cards = 1;
|
||||
double seconds = 2;
|
||||
}
|
||||
|
@ -1016,6 +1017,7 @@ message RevlogEntry {
|
|||
REVIEW = 1;
|
||||
RELEARNING = 2;
|
||||
EARLY_REVIEW = 3;
|
||||
MANUAL = 4;
|
||||
}
|
||||
int64 id = 1;
|
||||
int64 cid = 2;
|
||||
|
|
|
@ -516,6 +516,9 @@ table.review-log {{ {revlog_style} }}
|
|||
|
||||
return style + self.backend.card_stats(card_id)
|
||||
|
||||
def studied_today(self) -> str:
|
||||
return self.backend.studied_today()
|
||||
|
||||
# legacy
|
||||
|
||||
def cardStats(self, card: Card) -> str:
|
||||
|
|
|
@ -145,7 +145,9 @@ from revlog where id > ? """
|
|||
return "<b>" + str(s) + "</b>"
|
||||
|
||||
if cards:
|
||||
b += self.col.backend.studied_today(cards=cards, seconds=float(thetime))
|
||||
b += self.col.backend.studied_today_message(
|
||||
cards=cards, seconds=float(thetime)
|
||||
)
|
||||
# again/pass count
|
||||
b += "<br>" + _("Again count: %s") % bold(failed)
|
||||
if cards:
|
||||
|
|
|
@ -138,16 +138,7 @@ class DeckBrowser:
|
|||
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
||||
|
||||
def _renderStats(self):
|
||||
cards, thetime = self.mw.col.db.first(
|
||||
"""
|
||||
select count(), sum(time)/1000 from revlog
|
||||
where id > ?""",
|
||||
(self.mw.col.sched.dayCutoff - 86400) * 1000,
|
||||
)
|
||||
cards = cards or 0
|
||||
thetime = thetime or 0
|
||||
buf = self.mw.col.backend.studied_today(cards=cards, seconds=float(thetime))
|
||||
return buf
|
||||
return self.mw.col.studied_today()
|
||||
|
||||
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
||||
buf = """
|
||||
|
|
|
@ -21,3 +21,5 @@ card-stats-review-log-type-learn = Learn
|
|||
card-stats-review-log-type-review = Review
|
||||
card-stats-review-log-type-relearn = Relearn
|
||||
card-stats-review-log-type-filtered = Filtered
|
||||
card-stats-review-log-type-manual = Manual
|
||||
|
||||
|
|
|
@ -31,8 +31,9 @@ use crate::{
|
|||
RenderCardOutput,
|
||||
},
|
||||
sched::cutoff::local_minutes_west_for_stamp,
|
||||
sched::timespan::{answer_button_time, studied_today, time_span},
|
||||
sched::timespan::{answer_button_time, time_span},
|
||||
search::SortMode,
|
||||
stats::studied_today,
|
||||
sync::{
|
||||
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
|
||||
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
||||
|
@ -472,8 +473,17 @@ impl BackendService for Backend {
|
|||
})
|
||||
}
|
||||
|
||||
fn studied_today(&mut self, input: pb::StudiedTodayIn) -> BackendResult<pb::String> {
|
||||
Ok(studied_today(input.cards as usize, input.seconds as f32, &self.i18n).into())
|
||||
/// Fetch data from DB and return rendered string.
|
||||
fn studied_today(&mut self, _input: pb::Empty) -> BackendResult<pb::String> {
|
||||
self.with_col(|col| col.studied_today().map(Into::into))
|
||||
}
|
||||
|
||||
/// Message rendering only, for old graphs.
|
||||
fn studied_today_message(
|
||||
&mut self,
|
||||
input: pb::StudiedTodayMessageIn,
|
||||
) -> BackendResult<pb::String> {
|
||||
Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into())
|
||||
}
|
||||
|
||||
fn update_stats(&mut self, input: pb::UpdateStatsIn) -> BackendResult<Empty> {
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::define_newtype;
|
|||
use crate::err::{AnkiError, Result};
|
||||
use crate::notes::NoteID;
|
||||
use crate::{
|
||||
collection::Collection, config::SchedulerVersion, deckconf::INITIAL_EASE_FACTOR,
|
||||
timestamp::TimestampSecs, types::Usn, undo::Undoable,
|
||||
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
||||
undo::Undoable,
|
||||
};
|
||||
use num_enum::TryFromPrimitive;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
@ -200,32 +200,6 @@ impl Card {
|
|||
|
||||
self.original_due = 0;
|
||||
}
|
||||
|
||||
/// Remove the card from the (re)learning queue.
|
||||
/// This will reset cards in learning.
|
||||
/// Only used in the V1 scheduler.
|
||||
/// Unlike the legacy Python code, this sets the due# to 0 instead of
|
||||
/// one past the previous max due number.
|
||||
pub(crate) fn remove_from_learning(&mut self) {
|
||||
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ctype == CardType::Review {
|
||||
// reviews are removed from relearning
|
||||
self.due = self.original_due;
|
||||
self.original_due = 0;
|
||||
self.queue = CardQueue::Review;
|
||||
} else {
|
||||
// other cards are reset to new
|
||||
self.ctype = CardType::New;
|
||||
self.queue = CardQueue::New;
|
||||
self.interval = 0;
|
||||
self.due = 0;
|
||||
self.original_due = 0;
|
||||
self.ease_factor = INITIAL_EASE_FACTOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UpdateCardUndo(Card);
|
||||
|
|
|
@ -42,6 +42,7 @@ pub enum RevlogReviewKind {
|
|||
Review = 1,
|
||||
Relearning = 2,
|
||||
EarlyReview = 3,
|
||||
Manual = 4,
|
||||
}
|
||||
|
||||
impl Default for RevlogReviewKind {
|
||||
|
@ -59,3 +60,40 @@ impl RevlogEntry {
|
|||
}) as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
fn last_interval_for_revlog_todo(&self) -> i32 {
|
||||
self.interval as i32
|
||||
|
||||
// fixme: need to pass in delays for (re)learning
|
||||
// if let Some(delay) = self.current_learning_delay_seconds(&[]) {
|
||||
// -(delay as i32)
|
||||
// } else {
|
||||
// self.interval as i32
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn log_manually_scheduled_review(
|
||||
&mut self,
|
||||
card: &Card,
|
||||
usn: Usn,
|
||||
next_interval: u32,
|
||||
) -> Result<()> {
|
||||
println!("fixme: learning last_interval");
|
||||
// let deck = self.get_deck(card.deck_id)?.ok_or(AnkiError::NotFound)?;
|
||||
let entry = RevlogEntry {
|
||||
id: TimestampMillis::now(),
|
||||
cid: card.id,
|
||||
usn,
|
||||
button_chosen: 0,
|
||||
interval: next_interval as i32,
|
||||
last_interval: card.last_interval_for_revlog_todo(),
|
||||
ease_factor: card.ease_factor as u32,
|
||||
taken_millis: 0,
|
||||
review_kind: RevlogReviewKind::Manual,
|
||||
};
|
||||
self.storage.add_revlog_entry(&entry)
|
||||
}
|
||||
}
|
||||
|
|
58
rslib/src/sched/learning.rs
Normal file
58
rslib/src/sched/learning.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
card::{Card, CardQueue, CardType},
|
||||
deckconf::INITIAL_EASE_FACTOR,
|
||||
};
|
||||
|
||||
impl Card {
|
||||
/// Remove the card from the (re)learning queue.
|
||||
/// This will reset cards in learning.
|
||||
/// Only used in the V1 scheduler.
|
||||
/// Unlike the legacy Python code, this sets the due# to 0 instead of
|
||||
/// one past the previous max due number.
|
||||
pub(crate) fn remove_from_learning(&mut self) {
|
||||
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ctype == CardType::Review {
|
||||
// reviews are removed from relearning
|
||||
self.due = self.original_due;
|
||||
self.original_due = 0;
|
||||
self.queue = CardQueue::Review;
|
||||
} else {
|
||||
// other cards are reset to new
|
||||
self.ctype = CardType::New;
|
||||
self.queue = CardQueue::New;
|
||||
self.interval = 0;
|
||||
self.due = 0;
|
||||
self.original_due = 0;
|
||||
self.ease_factor = INITIAL_EASE_FACTOR;
|
||||
}
|
||||
}
|
||||
|
||||
fn all_remaining_steps(&self) -> u32 {
|
||||
self.remaining_steps % 1000
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn remaining_steps_today(&self) -> u32 {
|
||||
self.remaining_steps / 1000
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn current_learning_delay_seconds(&self, delays: &[u32]) -> Option<u32> {
|
||||
if self.queue == CardQueue::Learn {
|
||||
let remaining = self.all_remaining_steps();
|
||||
delays
|
||||
.iter()
|
||||
.nth_back(remaining.saturating_sub(0) as usize)
|
||||
.or(Some(&0))
|
||||
.map(|n| n * 60)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
pub mod bury_and_suspend;
|
||||
pub(crate) mod congrats;
|
||||
pub mod cutoff;
|
||||
mod learning;
|
||||
mod reviews;
|
||||
pub mod timespan;
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ impl Collection {
|
|||
for mut card in col.storage.all_searched_cards()? {
|
||||
let original = card.clone();
|
||||
let interval = distribution.sample(&mut rng);
|
||||
col.log_manually_scheduled_review(&card, usn, interval)?;
|
||||
card.schedule_as_review(interval, today);
|
||||
col.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
|
|
|
@ -41,21 +41,6 @@ pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String {
|
|||
i18n.trn(key, args)
|
||||
}
|
||||
|
||||
// fixme: this doesn't belong here
|
||||
pub fn studied_today(cards: usize, secs: f32, i18n: &I18n) -> String {
|
||||
let span = Timespan::from_secs(secs).natural_span();
|
||||
let amount = span.as_unit();
|
||||
let unit = span.unit().as_str();
|
||||
let secs_per = if cards > 0 {
|
||||
secs / (cards as f32)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let args = tr_args!["amount" => amount, "unit" => unit,
|
||||
"cards" => cards, "secs-per-card" => secs_per];
|
||||
i18n.trn(TR::StatisticsStudiedToday, args)
|
||||
}
|
||||
|
||||
const SECOND: f32 = 1.0;
|
||||
const MINUTE: f32 = 60.0 * SECOND;
|
||||
const HOUR: f32 = 60.0 * MINUTE;
|
||||
|
@ -64,7 +49,7 @@ const MONTH: f32 = 30.0 * DAY;
|
|||
const YEAR: f32 = 12.0 * MONTH;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TimespanUnit {
|
||||
pub(crate) enum TimespanUnit {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
|
@ -74,7 +59,7 @@ enum TimespanUnit {
|
|||
}
|
||||
|
||||
impl TimespanUnit {
|
||||
fn as_str(self) -> &'static str {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
TimespanUnit::Seconds => "seconds",
|
||||
TimespanUnit::Minutes => "minutes",
|
||||
|
@ -87,13 +72,13 @@ impl TimespanUnit {
|
|||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Timespan {
|
||||
pub(crate) struct Timespan {
|
||||
seconds: f32,
|
||||
unit: TimespanUnit,
|
||||
}
|
||||
|
||||
impl Timespan {
|
||||
fn from_secs(seconds: f32) -> Self {
|
||||
pub fn from_secs(seconds: f32) -> Self {
|
||||
Timespan {
|
||||
seconds,
|
||||
unit: TimespanUnit::Seconds,
|
||||
|
@ -102,7 +87,7 @@ impl Timespan {
|
|||
|
||||
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
|
||||
/// returns 1.17
|
||||
fn as_unit(self) -> f32 {
|
||||
pub fn as_unit(self) -> f32 {
|
||||
let s = self.seconds;
|
||||
match self.unit {
|
||||
TimespanUnit::Seconds => s,
|
||||
|
@ -116,7 +101,7 @@ impl Timespan {
|
|||
|
||||
/// Round seconds and days to integers, otherwise
|
||||
/// truncates to one decimal place.
|
||||
fn as_rounded_unit(self) -> f32 {
|
||||
pub fn as_rounded_unit(self) -> f32 {
|
||||
match self.unit {
|
||||
// seconds/days as integer
|
||||
TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(),
|
||||
|
@ -125,13 +110,13 @@ impl Timespan {
|
|||
}
|
||||
}
|
||||
|
||||
fn unit(self) -> TimespanUnit {
|
||||
pub fn unit(self) -> TimespanUnit {
|
||||
self.unit
|
||||
}
|
||||
|
||||
/// Return a new timespan in the most appropriate unit, eg
|
||||
/// 70 secs -> timespan in minutes
|
||||
fn natural_span(self) -> Timespan {
|
||||
pub fn natural_span(self) -> Timespan {
|
||||
let secs = self.seconds.abs();
|
||||
let unit = if secs < MINUTE {
|
||||
TimespanUnit::Seconds
|
||||
|
@ -158,7 +143,7 @@ impl Timespan {
|
|||
mod test {
|
||||
use crate::i18n::I18n;
|
||||
use crate::log;
|
||||
use crate::sched::timespan::{answer_button_time, studied_today, time_span, MONTH};
|
||||
use crate::sched::timespan::{answer_button_time, time_span, MONTH};
|
||||
|
||||
#[test]
|
||||
fn answer_buttons() {
|
||||
|
@ -180,15 +165,4 @@ mod test {
|
|||
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months");
|
||||
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &i18n, false), "1.5 years");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combo() {
|
||||
// temporary test of fluent term handling
|
||||
let log = log::terminal();
|
||||
let i18n = I18n::new(&["zz"], "", log);
|
||||
assert_eq!(
|
||||
&studied_today(3, 13.0, &i18n).replace("\n", " "),
|
||||
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,12 +229,14 @@ fn revlog_to_text(e: RevlogEntry, i18n: &I18n, offset: FixedOffset) -> RevlogTex
|
|||
RevlogReviewKind::Review => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
|
||||
RevlogReviewKind::Relearning => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
|
||||
RevlogReviewKind::EarlyReview => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
|
||||
RevlogReviewKind::Manual => i18n.tr(TR::CardStatsReviewLogTypeManual).into(),
|
||||
};
|
||||
let kind_class = match e.review_kind {
|
||||
RevlogReviewKind::Learning => String::from("revlog-learn"),
|
||||
RevlogReviewKind::Review => String::from("revlog-review"),
|
||||
RevlogReviewKind::Relearning => String::from("revlog-relearn"),
|
||||
RevlogReviewKind::EarlyReview => String::from("revlog-filtered"),
|
||||
RevlogReviewKind::Manual => String::from("revlog-manual"),
|
||||
};
|
||||
let rating = e.button_chosen.to_string();
|
||||
let interval = if e.interval == 0 {
|
||||
|
|
|
@ -3,3 +3,6 @@
|
|||
|
||||
mod card;
|
||||
mod graphs;
|
||||
mod today;
|
||||
|
||||
pub use today::studied_today;
|
||||
|
|
45
rslib/src/stats/today.rs
Normal file
45
rslib/src/stats/today.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{i18n::I18n, prelude::*, sched::timespan::Timespan};
|
||||
|
||||
pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String {
|
||||
let span = Timespan::from_secs(secs).natural_span();
|
||||
let amount = span.as_unit();
|
||||
let unit = span.unit().as_str();
|
||||
let secs_per = if cards > 0 {
|
||||
secs / (cards as f32)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let args = tr_args!["amount" => amount, "unit" => unit,
|
||||
"cards" => cards, "secs-per-card" => secs_per];
|
||||
i18n.trn(TR::StatisticsStudiedToday, args)
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn studied_today(&self) -> Result<String> {
|
||||
let today = self
|
||||
.storage
|
||||
.studied_today(self.timing_today()?.next_day_at)?;
|
||||
Ok(studied_today(today.cards, today.seconds as f32, &self.i18n))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::studied_today;
|
||||
use crate::i18n::I18n;
|
||||
use crate::log;
|
||||
|
||||
#[test]
|
||||
fn today() {
|
||||
// temporary test of fluent term handling
|
||||
let log = log::terminal();
|
||||
let i18n = I18n::new(&["zz"], "", log);
|
||||
assert_eq!(
|
||||
&studied_today(3, 13.0, &i18n).replace("\n", " "),
|
||||
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,5 +10,25 @@ insert
|
|||
time,
|
||||
type
|
||||
)
|
||||
values
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
values (
|
||||
(
|
||||
case
|
||||
when ?1 in (
|
||||
select id
|
||||
from revlog
|
||||
) then (
|
||||
select max(id) + 1
|
||||
from revlog
|
||||
)
|
||||
else ?1
|
||||
end
|
||||
),
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
)
|
|
@ -15,6 +15,11 @@ use rusqlite::{
|
|||
};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub(crate) struct StudiedToday {
|
||||
pub cards: u32,
|
||||
pub seconds: f64,
|
||||
}
|
||||
|
||||
impl FromSql for RevlogReviewKind {
|
||||
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
||||
if let ValueRef::Integer(i) = value {
|
||||
|
@ -113,4 +118,19 @@ impl SqliteStorage {
|
|||
})?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn studied_today(&self, day_cutoff: i64) -> Result<StudiedToday> {
|
||||
let start = (day_cutoff - 86_400) * 1_000;
|
||||
self.db
|
||||
.prepare_cached(include_str!("studied_today.sql"))?
|
||||
.query_map(&[start, RevlogReviewKind::Manual as i64], |row| {
|
||||
Ok(StudiedToday {
|
||||
cards: row.get(0)?,
|
||||
seconds: row.get(1)?,
|
||||
})
|
||||
})?
|
||||
.next()
|
||||
.unwrap()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
|
5
rslib/src/storage/revlog/studied_today.sql
Normal file
5
rslib/src/storage/revlog/studied_today.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
select count(),
|
||||
coalesce(sum(time) / 1000.0, 0.0)
|
||||
from revlog
|
||||
where id > ?
|
||||
and type != ?
|
|
@ -15,6 +15,10 @@ impl TimestampSecs {
|
|||
Self(elapsed().as_secs() as i64)
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn elapsed_secs(self) -> u64 {
|
||||
(Self::now().0 - self.0).max(0) as u64
|
||||
}
|
||||
|
@ -30,6 +34,10 @@ impl TimestampMillis {
|
|||
Self(elapsed().as_millis() as i64)
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn as_secs(self) -> TimestampSecs {
|
||||
TimestampSecs(self.0 / 1000)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
|||
const empty = { mature: 0, young: 0, learn: 0, relearn: 0, early: 0 };
|
||||
|
||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||
if (review.reviewKind == ReviewKind.MANUAL) {
|
||||
// don't count days with only manual scheduling
|
||||
continue;
|
||||
}
|
||||
const day = Math.ceil(
|
||||
((review.id as number) / 1000 - data.nextDayAtSecs) / 86400
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import pb, { BackendProto } from "../backend/proto";
|
||||
import { studiedToday } from "../time";
|
||||
import { I18n } from "../i18n";
|
||||
|
||||
|
@ -30,6 +30,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
|
|||
continue;
|
||||
}
|
||||
|
||||
if (review.reviewKind == ReviewKind.MANUAL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// total
|
||||
answerCount += 1;
|
||||
answerMillis += review.takenMillis;
|
||||
|
|
Loading…
Reference in a new issue