Anki/rslib/src/config/mod.rs
Damien Elmes 78a7620079 update undo skipping; exclude deck/tag expand/collapse
Instead of calling a method inside the transaction body, routines
can now pass Op::SkipUndo if they wish the changes to be discarded
at the end of the transaction. The advantage of doing it this way is
that the list of changes can still be returned, allowing the sync
indicator to update immediately.

Closes #1252
2021-06-25 09:16:15 +10:00

326 lines
9.2 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod bool;
mod deck;
mod notetype;
pub(crate) mod schema11;
mod string;
pub(crate) mod undo;
use serde::{de::DeserializeOwned, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use slog::warn;
use strum::IntoStaticStr;
pub use self::{bool::BoolKey, notetype::get_aux_notetype_config_key, string::StringKey};
use crate::prelude::*;
/// Only used when updating/undoing.
#[derive(Debug)]
pub(crate) struct ConfigEntry {
pub key: String,
pub value: Vec<u8>,
pub usn: Usn,
pub mtime: TimestampSecs,
}
impl ConfigEntry {
pub(crate) fn boxed(key: &str, value: Vec<u8>, usn: Usn, mtime: TimestampSecs) -> Box<Self> {
Box::new(Self {
key: key.into(),
value,
usn,
mtime,
})
}
}
#[derive(IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
pub(crate) enum ConfigKey {
CreationOffset,
FirstDayOfWeek,
LocalOffset,
Rollover,
#[strum(to_string = "timeLim")]
AnswerTimeLimitSecs,
#[strum(to_string = "curDeck")]
CurrentDeckId,
#[strum(to_string = "curModel")]
CurrentNotetypeId,
#[strum(to_string = "lastUnburied")]
LastUnburiedDay,
#[strum(to_string = "collapseTime")]
LearnAheadSecs,
#[strum(to_string = "newSpread")]
NewReviewMix,
#[strum(to_string = "nextPos")]
NextNewCardPosition,
#[strum(to_string = "schedVer")]
SchedulerVersion,
}
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
#[repr(u8)]
pub enum SchedulerVersion {
V1 = 1,
V2 = 2,
}
impl Collection {
pub fn set_config_json<T: Serialize>(
&mut self,
key: &str,
val: &T,
undoable: bool,
) -> Result<OpOutput<()>> {
let op = if undoable {
Op::UpdateConfig
} else {
Op::SkipUndo
};
self.transact(op, |col| {
col.set_config(key, val)?;
Ok(())
})
}
pub fn remove_config(&mut self, key: &str) -> Result<OpOutput<()>> {
self.transact(Op::UpdateConfig, |col| col.remove_config_inner(key))
}
}
impl Collection {
/// Get config item, returning None if missing/invalid.
pub(crate) fn get_config_optional<'a, T, K>(&self, key: K) -> Option<T>
where
T: DeserializeOwned,
K: Into<&'a str>,
{
let key = key.into();
match self.storage.get_config_value(key) {
Ok(Some(val)) => Some(val),
Ok(None) => None,
Err(e) => {
warn!(self.log, "error accessing config key"; "key"=>key, "err"=>?e);
None
}
}
}
// /// Get config item, returning default value if missing/invalid.
pub(crate) fn get_config_default<T, K>(&self, key: K) -> T
where
T: DeserializeOwned + Default,
K: Into<&'static str>,
{
self.get_config_optional(key).unwrap_or_default()
}
/// True if added, or new value is different.
pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<bool>
where
K: Into<&'a str>,
{
let entry = ConfigEntry::boxed(
key.into(),
serde_json::to_vec(val)?,
self.usn()?,
TimestampSecs::now(),
);
self.set_config_undoable(entry)
}
pub(crate) fn remove_config_inner<'a, K>(&mut self, key: K) -> Result<()>
where
K: Into<&'a str>,
{
self.remove_config_undoable(key.into())
}
/// Remove all keys starting with provided prefix, which must end with '_'.
pub(crate) fn remove_config_prefix(&mut self, key: &str) -> Result<()> {
for (key, _val) in self.storage.get_config_prefix(key)? {
self.remove_config_inner(key.as_str())?;
}
Ok(())
}
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
self.get_config_optional(ConfigKey::CreationOffset)
}
pub(crate) fn set_creation_utc_offset(&mut self, mins: Option<i32>) -> Result<()> {
self.state.scheduler_info = None;
if let Some(mins) = mins {
self.set_config(ConfigKey::CreationOffset, &mins)
.map(|_| ())
} else {
self.remove_config_inner(ConfigKey::CreationOffset)
}
}
pub(crate) fn get_configured_utc_offset(&self) -> Option<i32> {
self.get_config_optional(ConfigKey::LocalOffset)
}
pub(crate) fn set_configured_utc_offset(&mut self, mins: i32) -> Result<()> {
self.state.scheduler_info = None;
self.set_config(ConfigKey::LocalOffset, &mins).map(|_| ())
}
pub(crate) fn get_v2_rollover(&self) -> Option<u8> {
self.get_config_optional::<u8, _>(ConfigKey::Rollover)
.map(|r| r.min(23))
}
pub(crate) fn set_v2_rollover(&mut self, hour: u32) -> Result<()> {
self.state.scheduler_info = None;
self.set_config(ConfigKey::Rollover, &hour).map(|_| ())
}
pub(crate) fn get_next_card_position(&self) -> u32 {
self.get_config_default(ConfigKey::NextNewCardPosition)
}
pub(crate) fn get_and_update_next_card_position(&mut self) -> Result<u32> {
let pos: u32 = self
.get_config_optional(ConfigKey::NextNewCardPosition)
.unwrap_or_default();
self.set_config(ConfigKey::NextNewCardPosition, &pos.wrapping_add(1))?;
Ok(pos)
}
pub(crate) fn set_next_card_position(&mut self, pos: u32) -> Result<()> {
self.set_config(ConfigKey::NextNewCardPosition, &pos)
.map(|_| ())
}
pub(crate) fn scheduler_version(&self) -> SchedulerVersion {
self.get_config_optional(ConfigKey::SchedulerVersion)
.unwrap_or(SchedulerVersion::V1)
}
/// Caution: this only updates the config setting.
pub(crate) fn set_scheduler_version_config_key(&mut self, ver: SchedulerVersion) -> Result<()> {
self.state.scheduler_info = None;
self.set_config(ConfigKey::SchedulerVersion, &ver)
.map(|_| ())
}
pub(crate) fn learn_ahead_secs(&self) -> u32 {
self.get_config_optional(ConfigKey::LearnAheadSecs)
.unwrap_or(1200)
}
pub(crate) fn set_learn_ahead_secs(&mut self, secs: u32) -> Result<()> {
self.set_config(ConfigKey::LearnAheadSecs, &secs)
.map(|_| ())
}
pub(crate) fn get_new_review_mix(&self) -> NewReviewMix {
match self.get_config_default::<u8, _>(ConfigKey::NewReviewMix) {
1 => NewReviewMix::ReviewsFirst,
2 => NewReviewMix::NewFirst,
_ => NewReviewMix::Mix,
}
}
pub(crate) fn set_new_review_mix(&mut self, mix: NewReviewMix) -> Result<()> {
self.set_config(ConfigKey::NewReviewMix, &(mix as u8))
.map(|_| ())
}
pub(crate) fn get_first_day_of_week(&self) -> Weekday {
self.get_config_optional(ConfigKey::FirstDayOfWeek)
.unwrap_or(Weekday::Sunday)
}
pub(crate) fn set_first_day_of_week(&mut self, weekday: Weekday) -> Result<()> {
self.set_config(ConfigKey::FirstDayOfWeek, &weekday)
.map(|_| ())
}
pub(crate) fn get_answer_time_limit_secs(&self) -> u32 {
self.get_config_optional(ConfigKey::AnswerTimeLimitSecs)
.unwrap_or_default()
}
pub(crate) fn set_answer_time_limit_secs(&mut self, secs: u32) -> Result<()> {
self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs)
.map(|_| ())
}
pub(crate) fn get_last_unburied_day(&self) -> u32 {
self.get_config_optional(ConfigKey::LastUnburiedDay)
.unwrap_or_default()
}
pub(crate) fn set_last_unburied_day(&mut self, day: u32) -> Result<()> {
self.set_config(ConfigKey::LastUnburiedDay, &day)
.map(|_| ())
}
}
// 2021 scheduler moves this into deck config
pub(crate) enum NewReviewMix {
Mix = 0,
ReviewsFirst = 1,
NewFirst = 2,
}
impl Default for NewReviewMix {
fn default() -> Self {
NewReviewMix::Mix
}
}
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy)]
#[repr(u8)]
pub(crate) enum Weekday {
Sunday = 0,
Monday = 1,
Friday = 5,
Saturday = 6,
}
#[cfg(test)]
mod test {
use crate::{collection::open_test_collection, decks::DeckId};
#[test]
fn defaults() {
let col = open_test_collection();
assert_eq!(col.get_current_deck_id(), DeckId(1));
}
#[test]
fn get_set() {
let mut col = open_test_collection();
// missing key
assert_eq!(col.get_config_optional::<Vec<i64>, _>("test"), None);
// normal retrieval
col.set_config("test", &vec![1, 2]).unwrap();
assert_eq!(
col.get_config_optional::<Vec<i64>, _>("test"),
Some(vec![1, 2])
);
// invalid type conversion
assert_eq!(col.get_config_optional::<i64, _>("test"), None,);
// invalid json
col.storage
.db
.execute(
"update config set val=? where key='test'",
&[b"xx".as_ref()],
)
.unwrap();
assert_eq!(col.get_config_optional::<i64, _>("test"), None,);
}
}