mirror of
https://github.com/ankitects/anki.git
synced 2025-11-07 05:07:10 -05:00
move answer_button_time to the backend, split sched into separate module
This commit is contained in:
parent
7b26814922
commit
99c07cfdcb
9 changed files with 171 additions and 27 deletions
|
|
@ -44,6 +44,7 @@ message BackendInput {
|
||||||
Empty check_media = 28;
|
Empty check_media = 28;
|
||||||
TrashMediaFilesIn trash_media_files = 29;
|
TrashMediaFilesIn trash_media_files = 29;
|
||||||
TranslateStringIn translate_string = 30;
|
TranslateStringIn translate_string = 30;
|
||||||
|
FormatTimeSpanIn format_time_span = 31;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ message BackendOutput {
|
||||||
MediaCheckOut check_media = 28;
|
MediaCheckOut check_media = 28;
|
||||||
Empty trash_media_files = 29;
|
Empty trash_media_files = 29;
|
||||||
string translate_string = 30;
|
string translate_string = 30;
|
||||||
|
string format_time_span = 31;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
|
|
@ -303,3 +305,12 @@ message TranslateArgValue {
|
||||||
double number = 2;
|
double number = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message FormatTimeSpanIn {
|
||||||
|
enum Context {
|
||||||
|
ANSWER_BUTTONS = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float seconds = 1;
|
||||||
|
Context context = 2;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,8 @@ MediaCheckOutput = pb.MediaCheckOut
|
||||||
|
|
||||||
StringsGroup = pb.StringsGroup
|
StringsGroup = pb.StringsGroup
|
||||||
|
|
||||||
|
FormatTimeSpanContext = pb.FormatTimeSpanIn.Context
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExtractedLatex:
|
class ExtractedLatex:
|
||||||
|
|
@ -342,3 +344,10 @@ class RustBackend:
|
||||||
translate_string=pb.TranslateStringIn(group=group, key=key, args=args)
|
translate_string=pb.TranslateStringIn(group=group, key=key, args=args)
|
||||||
)
|
)
|
||||||
).translate_string
|
).translate_string
|
||||||
|
|
||||||
|
def format_time_span(self, seconds: float, context: FormatTimeSpanContext) -> str:
|
||||||
|
return self._run_command(
|
||||||
|
pb.BackendInput(
|
||||||
|
format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context)
|
||||||
|
)
|
||||||
|
).format_time_span
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
@ -8,11 +10,13 @@ from heapq import *
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import List, Optional, Set
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
|
import anki
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.utils import answer_button_time, ids2str, intTime
|
from anki.rsbackend import FormatTimeSpanContext
|
||||||
|
from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
|
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
|
||||||
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
||||||
|
|
@ -25,7 +29,7 @@ class Scheduler:
|
||||||
_spreadRev = True
|
_spreadRev = True
|
||||||
_burySiblingsOnAnswer = True
|
_burySiblingsOnAnswer = True
|
||||||
|
|
||||||
def __init__(self, col):
|
def __init__(self, col: anki.storage._Collection) -> None:
|
||||||
self.col = col
|
self.col = col
|
||||||
self.queueLimit = 50
|
self.queueLimit = 50
|
||||||
self.reportLimit = 1000
|
self.reportLimit = 1000
|
||||||
|
|
@ -33,7 +37,7 @@ class Scheduler:
|
||||||
self.lrnCount = 0
|
self.lrnCount = 0
|
||||||
self.revCount = 0
|
self.revCount = 0
|
||||||
self.newCount = 0
|
self.newCount = 0
|
||||||
self.today = None
|
self.today: Optional[int] = None
|
||||||
self._haveQueues = False
|
self._haveQueues = False
|
||||||
self._updateCutoff()
|
self._updateCutoff()
|
||||||
|
|
||||||
|
|
@ -1354,7 +1358,9 @@ To study outside of the normal schedule, click the Custom Study button below."""
|
||||||
ivl_secs = self.nextIvl(card, ease)
|
ivl_secs = self.nextIvl(card, ease)
|
||||||
if not ivl_secs:
|
if not ivl_secs:
|
||||||
return _("(end)")
|
return _("(end)")
|
||||||
s = answer_button_time(self.col, ivl_secs)
|
s = self.col.backend.format_time_span(
|
||||||
|
ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS
|
||||||
|
)
|
||||||
if ivl_secs < self.col.conf["collapseTime"]:
|
if ivl_secs < self.col.conf["collapseTime"]:
|
||||||
s = "<" + s
|
s = "<" + s
|
||||||
return s
|
return s
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.rsbackend import SchedTimingToday
|
from anki.rsbackend import FormatTimeSpanContext, SchedTimingToday
|
||||||
from anki.utils import answer_button_time, ids2str, intTime
|
from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
||||||
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
||||||
|
|
@ -1548,7 +1548,9 @@ To study outside of the normal schedule, click the Custom Study button below."""
|
||||||
ivl_secs = self.nextIvl(card, ease)
|
ivl_secs = self.nextIvl(card, ease)
|
||||||
if not ivl_secs:
|
if not ivl_secs:
|
||||||
return _("(end)")
|
return _("(end)")
|
||||||
s = answer_button_time(self.col, ivl_secs)
|
s = self.col.backend.format_time_span(
|
||||||
|
ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS
|
||||||
|
)
|
||||||
if ivl_secs < self.col.conf["collapseTime"]:
|
if ivl_secs < self.col.conf["collapseTime"]:
|
||||||
s = "<" + s
|
s = "<" + s
|
||||||
return s
|
return s
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ from hashlib import sha1
|
||||||
from html.entities import name2codepoint
|
from html.entities import name2codepoint
|
||||||
from typing import Iterable, Iterator, List, Optional, Tuple, Union
|
from typing import Iterable, Iterator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import anki
|
|
||||||
from anki.backend_pb2 import StringsGroup
|
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
|
|
||||||
|
|
@ -39,22 +37,6 @@ def intTime(scale: int = 1) -> int:
|
||||||
return int(time.time() * scale)
|
return int(time.time() * scale)
|
||||||
|
|
||||||
|
|
||||||
# eg 70 seconds -> (1.16, "minutes")
|
|
||||||
def seconds_to_appropriate_unit(seconds: int) -> Tuple[float, str]:
|
|
||||||
unit, _ = optimalPeriod(seconds, 0, 99)
|
|
||||||
amount = convertSecondsTo(seconds, unit)
|
|
||||||
return (amount, unit)
|
|
||||||
|
|
||||||
|
|
||||||
def answer_button_time(col: anki.storage._Collection, seconds: int) -> str:
|
|
||||||
(amount, unit) = seconds_to_appropriate_unit(seconds)
|
|
||||||
if unit not in ("months", "years"):
|
|
||||||
amount = int(amount)
|
|
||||||
return col.backend.translate(
|
|
||||||
StringsGroup.SCHEDULING, f"answer-button-time-{unit}", amount=amount
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
timeTable = {
|
timeTable = {
|
||||||
"years": lambda n: ngettext("%s year", "%s years", n),
|
"years": lambda n: ngettext("%s year", "%s years", n),
|
||||||
"months": lambda n: ngettext("%s month", "%s months", n),
|
"months": lambda n: ngettext("%s month", "%s months", n),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ use crate::latex::{extract_latex, ExtractedLatex};
|
||||||
use crate::media::check::MediaChecker;
|
use crate::media::check::MediaChecker;
|
||||||
use crate::media::sync::MediaSyncProgress;
|
use crate::media::sync::MediaSyncProgress;
|
||||||
use crate::media::MediaManager;
|
use crate::media::MediaManager;
|
||||||
use crate::sched::{local_minutes_west_for_stamp, sched_timing_today};
|
use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today};
|
||||||
|
use crate::sched::timespan::answer_button_time;
|
||||||
use crate::template::{
|
use crate::template::{
|
||||||
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
|
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
|
||||||
RenderedNode,
|
RenderedNode,
|
||||||
|
|
@ -200,6 +201,7 @@ impl Backend {
|
||||||
OValue::TrashMediaFiles(Empty {})
|
OValue::TrashMediaFiles(Empty {})
|
||||||
}
|
}
|
||||||
Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)),
|
Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)),
|
||||||
|
Value::FormatTimeSpan(input) => OValue::FormatTimeSpan(self.format_time_span(input)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,6 +399,18 @@ impl Backend {
|
||||||
|
|
||||||
self.i18n.get(group).trn(&input.key, map)
|
self.i18n.get(group).trn(&input.key, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_time_span(&self, input: pb::FormatTimeSpanIn) -> String {
|
||||||
|
let context = match pb::format_time_span_in::Context::from_i32(input.context) {
|
||||||
|
Some(context) => context,
|
||||||
|
None => return "".to_string(),
|
||||||
|
};
|
||||||
|
match context {
|
||||||
|
pb::format_time_span_in::Context::AnswerButtons => {
|
||||||
|
answer_button_time(input.seconds, &self.i18n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ pub fn local_minutes_west_for_stamp(stamp: i64) -> i32 {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::sched::{
|
use crate::sched::cutoff::{
|
||||||
fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour,
|
fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour,
|
||||||
sched_timing_today,
|
sched_timing_today,
|
||||||
};
|
};
|
||||||
2
rslib/src/sched/mod.rs
Normal file
2
rslib/src/sched/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod cutoff;
|
||||||
|
pub mod timespan;
|
||||||
118
rslib/src/sched/timespan.rs
Normal file
118
rslib/src/sched/timespan.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::i18n::{tr_args, I18n, StringsGroup};
|
||||||
|
|
||||||
|
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
|
||||||
|
let span = Timespan::from_secs(seconds).natural_span();
|
||||||
|
let amount = match span.unit() {
|
||||||
|
TimespanUnit::Months | TimespanUnit::Years => span.as_unit(),
|
||||||
|
// we don't show fractional value except for months/years
|
||||||
|
_ => span.as_unit().round(),
|
||||||
|
};
|
||||||
|
let unit = span.unit().as_str();
|
||||||
|
let args = tr_args!["amount" => amount];
|
||||||
|
i18n.get(StringsGroup::Scheduling)
|
||||||
|
.trn(&format!("answer-button-time-{}", unit), args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECOND: f32 = 1.0;
|
||||||
|
const MINUTE: f32 = 60.0 * SECOND;
|
||||||
|
const HOUR: f32 = 60.0 * MINUTE;
|
||||||
|
const DAY: f32 = 24.0 * HOUR;
|
||||||
|
const MONTH: f32 = 30.0 * DAY;
|
||||||
|
const YEAR: f32 = 365.0 * MONTH;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum TimespanUnit {
|
||||||
|
Seconds,
|
||||||
|
Minutes,
|
||||||
|
Hours,
|
||||||
|
Days,
|
||||||
|
Months,
|
||||||
|
Years,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimespanUnit {
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TimespanUnit::Seconds => "seconds",
|
||||||
|
TimespanUnit::Minutes => "minutes",
|
||||||
|
TimespanUnit::Hours => "hours",
|
||||||
|
TimespanUnit::Days => "days",
|
||||||
|
TimespanUnit::Months => "months",
|
||||||
|
TimespanUnit::Years => "years",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Timespan {
|
||||||
|
seconds: f32,
|
||||||
|
unit: TimespanUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timespan {
|
||||||
|
fn from_secs(seconds: f32) -> Self {
|
||||||
|
Timespan {
|
||||||
|
seconds,
|
||||||
|
unit: TimespanUnit::Seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
|
||||||
|
/// returns 1.17
|
||||||
|
fn as_unit(self) -> f32 {
|
||||||
|
let s = self.seconds;
|
||||||
|
match self.unit {
|
||||||
|
TimespanUnit::Seconds => s,
|
||||||
|
TimespanUnit::Minutes => s / MINUTE,
|
||||||
|
TimespanUnit::Hours => s / HOUR,
|
||||||
|
TimespanUnit::Days => s / DAY,
|
||||||
|
TimespanUnit::Months => s / MONTH,
|
||||||
|
TimespanUnit::Years => s / YEAR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let secs = self.seconds.abs();
|
||||||
|
let unit = if secs < MINUTE {
|
||||||
|
TimespanUnit::Seconds
|
||||||
|
} else if secs < HOUR {
|
||||||
|
TimespanUnit::Minutes
|
||||||
|
} else if secs < DAY {
|
||||||
|
TimespanUnit::Hours
|
||||||
|
} else if secs < MONTH {
|
||||||
|
TimespanUnit::Days
|
||||||
|
} else if secs < YEAR {
|
||||||
|
TimespanUnit::Months
|
||||||
|
} else {
|
||||||
|
TimespanUnit::Years
|
||||||
|
};
|
||||||
|
|
||||||
|
Timespan {
|
||||||
|
seconds: self.seconds,
|
||||||
|
unit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::i18n::I18n;
|
||||||
|
use crate::sched::timespan::{answer_button_time, MONTH};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn answer_buttons() {
|
||||||
|
let i18n = I18n::new(&["zz"], "");
|
||||||
|
assert_eq!(answer_button_time(30.0, &i18n), "30s");
|
||||||
|
assert_eq!(answer_button_time(70.0, &i18n), "1m");
|
||||||
|
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.10mo");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue