mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
move deck conf handling to backend
This commit is contained in:
parent
5b26b20697
commit
004cc2b5f8
15 changed files with 411 additions and 53 deletions
|
@ -50,6 +50,11 @@ message BackendInput {
|
||||||
int64 get_card = 38;
|
int64 get_card = 38;
|
||||||
Card update_card = 39;
|
Card update_card = 39;
|
||||||
Card add_card = 40;
|
Card add_card = 40;
|
||||||
|
int64 get_deck_config = 41;
|
||||||
|
string add_or_update_deck_config = 42;
|
||||||
|
Empty all_deck_config = 43;
|
||||||
|
Empty new_deck_config = 44;
|
||||||
|
int64 remove_deck_config = 45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +88,11 @@ message BackendOutput {
|
||||||
GetCardOut get_card = 38;
|
GetCardOut get_card = 38;
|
||||||
Empty update_card = 39;
|
Empty update_card = 39;
|
||||||
int64 add_card = 40;
|
int64 add_card = 40;
|
||||||
|
string get_deck_config = 41;
|
||||||
|
int64 add_or_update_deck_config = 42;
|
||||||
|
string all_deck_config = 43;
|
||||||
|
string new_deck_config = 44;
|
||||||
|
Empty remove_deck_config = 45;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,6 @@ defaultConf = {
|
||||||
|
|
||||||
class DeckManager:
|
class DeckManager:
|
||||||
decks: Dict[str, Any]
|
decks: Dict[str, Any]
|
||||||
dconf: Dict[str, Any]
|
|
||||||
|
|
||||||
# Registry save/load
|
# Registry save/load
|
||||||
#############################################################
|
#############################################################
|
||||||
|
@ -103,26 +102,19 @@ class DeckManager:
|
||||||
def __init__(self, col: anki.storage._Collection) -> None:
|
def __init__(self, col: anki.storage._Collection) -> None:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
self.decks = {}
|
self.decks = {}
|
||||||
self.dconf = {}
|
|
||||||
|
|
||||||
def load(self, decks: str, dconf: str) -> None:
|
def load(self, decks: str, dconf: str) -> None:
|
||||||
self.decks = json.loads(decks)
|
self.decks = json.loads(decks)
|
||||||
self.dconf = json.loads(dconf)
|
|
||||||
# set limits to within bounds
|
|
||||||
found = False
|
|
||||||
for c in list(self.dconf.values()):
|
|
||||||
for t in ("rev", "new"):
|
|
||||||
pd = "perDay"
|
|
||||||
if c[t][pd] > 999999:
|
|
||||||
c[t][pd] = 999999
|
|
||||||
self.save(c)
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
self.changed = False
|
self.changed = False
|
||||||
|
|
||||||
def save(self, g: Optional[Any] = None) -> None:
|
def save(self, g: Optional[Any] = None) -> None:
|
||||||
"Can be called with either a deck or a deck configuration."
|
"Can be called with either a deck or a deck configuration."
|
||||||
if g:
|
if g:
|
||||||
|
# deck conf?
|
||||||
|
if "maxTaken" in g:
|
||||||
|
self.updateConf(g)
|
||||||
|
return
|
||||||
|
else:
|
||||||
g["mod"] = intTime()
|
g["mod"] = intTime()
|
||||||
g["usn"] = self.col.usn()
|
g["usn"] = self.col.usn()
|
||||||
self.changed = True
|
self.changed = True
|
||||||
|
@ -130,9 +122,7 @@ class DeckManager:
|
||||||
def flush(self) -> None:
|
def flush(self) -> None:
|
||||||
if self.changed:
|
if self.changed:
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"update col set decks=?, dconf=?",
|
"update col set decks=?", json.dumps(self.decks),
|
||||||
json.dumps(self.decks),
|
|
||||||
json.dumps(self.dconf),
|
|
||||||
)
|
)
|
||||||
self.changed = False
|
self.changed = False
|
||||||
|
|
||||||
|
@ -368,7 +358,7 @@ class DeckManager:
|
||||||
|
|
||||||
def allConf(self) -> List:
|
def allConf(self) -> List:
|
||||||
"A list of all deck config."
|
"A list of all deck config."
|
||||||
return list(self.dconf.values())
|
return list(self.col.backend.all_deck_config().values())
|
||||||
|
|
||||||
def confForDid(self, did: int) -> Any:
|
def confForDid(self, did: int) -> Any:
|
||||||
deck = self.get(did, default=False)
|
deck = self.get(did, default=False)
|
||||||
|
@ -381,32 +371,25 @@ class DeckManager:
|
||||||
return deck
|
return deck
|
||||||
|
|
||||||
def getConf(self, confId: int) -> Any:
|
def getConf(self, confId: int) -> Any:
|
||||||
return self.dconf[str(confId)]
|
return self.col.backend.get_deck_config(confId)
|
||||||
|
|
||||||
def updateConf(self, g: Dict[str, Any]) -> None:
|
def updateConf(self, g: Dict[str, Any]) -> None:
|
||||||
self.dconf[str(g["id"])] = g
|
self.col.backend.add_or_update_deck_config(g)
|
||||||
self.save()
|
|
||||||
|
|
||||||
def confId(self, name: str, cloneFrom: Optional[Dict[str, Any]] = None) -> int:
|
def confId(self, name: str, cloneFrom: Optional[Dict[str, Any]] = None) -> int:
|
||||||
"Create a new configuration and return id."
|
"Create a new configuration and return id."
|
||||||
if cloneFrom is None:
|
if cloneFrom is not None:
|
||||||
cloneFrom = defaultConf
|
conf = copy.deepcopy(cloneFrom)
|
||||||
c = copy.deepcopy(cloneFrom)
|
conf["id"] = 0
|
||||||
while 1:
|
else:
|
||||||
id = intTime(1000)
|
conf = self.col.backend.new_deck_config()
|
||||||
if str(id) not in self.dconf:
|
conf["name"] = name
|
||||||
break
|
self.updateConf(conf)
|
||||||
c["id"] = id
|
return conf["id"]
|
||||||
c["name"] = name
|
|
||||||
self.dconf[str(id)] = c
|
|
||||||
self.save(c)
|
|
||||||
return id
|
|
||||||
|
|
||||||
def remConf(self, id) -> None:
|
def remConf(self, id) -> None:
|
||||||
"Remove a configuration and update all decks using it."
|
"Remove a configuration and update all decks using it."
|
||||||
assert int(id) != 1
|
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
del self.dconf[str(id)]
|
|
||||||
for g in self.all():
|
for g in self.all():
|
||||||
# ignore cram decks
|
# ignore cram decks
|
||||||
if "conf" not in g:
|
if "conf" not in g:
|
||||||
|
@ -414,6 +397,7 @@ class DeckManager:
|
||||||
if str(g["conf"]) == str(id):
|
if str(g["conf"]) == str(id):
|
||||||
g["conf"] = 1
|
g["conf"] = 1
|
||||||
self.save(g)
|
self.save(g)
|
||||||
|
self.col.backend.remove_deck_config(id)
|
||||||
|
|
||||||
def setConf(self, grp: Dict[str, Any], id: int) -> None:
|
def setConf(self, grp: Dict[str, Any], id: int) -> None:
|
||||||
grp["conf"] = id
|
grp["conf"] = id
|
||||||
|
@ -428,11 +412,10 @@ class DeckManager:
|
||||||
|
|
||||||
def restoreToDefault(self, conf) -> None:
|
def restoreToDefault(self, conf) -> None:
|
||||||
oldOrder = conf["new"]["order"]
|
oldOrder = conf["new"]["order"]
|
||||||
new = copy.deepcopy(defaultConf)
|
new = self.col.backend.new_deck_config()
|
||||||
new["id"] = conf["id"]
|
new["id"] = conf["id"]
|
||||||
new["name"] = conf["name"]
|
new["name"] = conf["name"]
|
||||||
self.dconf[str(conf["id"])] = new
|
self.updateConf(new)
|
||||||
self.save(new)
|
|
||||||
# if it was previously randomized, resort
|
# if it was previously randomized, resort
|
||||||
if not oldOrder:
|
if not oldOrder:
|
||||||
self.col.sched.resortConf(new)
|
self.col.sched.resortConf(new)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
# pylint: skip-file
|
# pylint: skip-file
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -43,7 +44,6 @@ try:
|
||||||
except:
|
except:
|
||||||
# add compat layer for 32 bit builds that can't use orjson
|
# add compat layer for 32 bit builds that can't use orjson
|
||||||
print("reverting to stock json")
|
print("reverting to stock json")
|
||||||
import json
|
|
||||||
|
|
||||||
class orjson: # type: ignore
|
class orjson: # type: ignore
|
||||||
def dumps(obj: Any) -> bytes:
|
def dumps(obj: Any) -> bytes:
|
||||||
|
@ -490,6 +490,32 @@ class RustBackend:
|
||||||
def add_card(self, card: BackendCard) -> None:
|
def add_card(self, card: BackendCard) -> None:
|
||||||
card.id = self._run_command(pb.BackendInput(add_card=card)).add_card
|
card.id = self._run_command(pb.BackendInput(add_card=card)).add_card
|
||||||
|
|
||||||
|
def get_deck_config(self, dcid: int) -> Dict[str, Any]:
|
||||||
|
jstr = self._run_command(pb.BackendInput(get_deck_config=dcid)).get_deck_config
|
||||||
|
return json.loads(jstr)
|
||||||
|
|
||||||
|
def add_or_update_deck_config(self, conf: Dict[str, Any]) -> None:
|
||||||
|
conf_json = json.dumps(conf)
|
||||||
|
id = self._run_command(
|
||||||
|
pb.BackendInput(add_or_update_deck_config=conf_json)
|
||||||
|
).add_or_update_deck_config
|
||||||
|
conf["id"] = id
|
||||||
|
|
||||||
|
def all_deck_config(self) -> Dict[int, Dict[str, Any]]:
|
||||||
|
jstr = self._run_command(
|
||||||
|
pb.BackendInput(all_deck_config=pb.Empty())
|
||||||
|
).all_deck_config
|
||||||
|
return json.loads(jstr)
|
||||||
|
|
||||||
|
def new_deck_config(self) -> Dict[str, Any]:
|
||||||
|
jstr = self._run_command(
|
||||||
|
pb.BackendInput(new_deck_config=pb.Empty())
|
||||||
|
).new_deck_config
|
||||||
|
return json.loads(jstr)
|
||||||
|
|
||||||
|
def remove_deck_config(self, dcid: int) -> None:
|
||||||
|
self._run_command(pb.BackendInput(remove_deck_config=dcid))
|
||||||
|
|
||||||
|
|
||||||
def translate_string_in(
|
def translate_string_in(
|
||||||
key: TR, **kwargs: Union[str, int, float]
|
key: TR, **kwargs: Union[str, int, float]
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::card::{Card, CardID};
|
||||||
use crate::card::{CardQueue, CardType};
|
use crate::card::{CardQueue, CardType};
|
||||||
use crate::collection::{open_collection, Collection};
|
use crate::collection::{open_collection, Collection};
|
||||||
use crate::config::SortKind;
|
use crate::config::SortKind;
|
||||||
|
use crate::deckconf::{DeckConf, DeckConfID};
|
||||||
use crate::decks::DeckID;
|
use crate::decks::DeckID;
|
||||||
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
|
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
|
||||||
use crate::i18n::{tr_args, FString, I18n};
|
use crate::i18n::{tr_args, FString, I18n};
|
||||||
|
@ -68,6 +69,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
||||||
AnkiError::Interrupted => V::Interrupted(Empty {}),
|
AnkiError::Interrupted => V::Interrupted(Empty {}),
|
||||||
AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}),
|
AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}),
|
||||||
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
|
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
|
||||||
|
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
|
@ -261,6 +263,16 @@ impl Backend {
|
||||||
OValue::UpdateCard(pb::Empty {})
|
OValue::UpdateCard(pb::Empty {})
|
||||||
}
|
}
|
||||||
Value::AddCard(card) => OValue::AddCard(self.add_card(card)?),
|
Value::AddCard(card) => OValue::AddCard(self.add_card(card)?),
|
||||||
|
Value::GetDeckConfig(dcid) => OValue::GetDeckConfig(self.get_deck_config(dcid)?),
|
||||||
|
Value::AddOrUpdateDeckConfig(conf_json) => {
|
||||||
|
OValue::AddOrUpdateDeckConfig(self.add_or_update_deck_config(conf_json)?)
|
||||||
|
}
|
||||||
|
Value::AllDeckConfig(_) => OValue::AllDeckConfig(self.all_deck_config()?),
|
||||||
|
Value::NewDeckConfig(_) => OValue::NewDeckConfig(self.new_deck_config()?),
|
||||||
|
Value::RemoveDeckConfig(dcid) => {
|
||||||
|
self.remove_deck_config(dcid)?;
|
||||||
|
OValue::RemoveDeckConfig(pb::Empty {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -651,6 +663,37 @@ impl Backend {
|
||||||
self.with_col(|col| col.transact(None, |ctx| ctx.add_card(&mut card)))?;
|
self.with_col(|col| col.transact(None, |ctx| ctx.add_card(&mut card)))?;
|
||||||
Ok(card.id.0)
|
Ok(card.id.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_deck_config(&self, dcid: i64) -> Result<String> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let conf = col.get_deck_config(DeckConfID(dcid), true)?.unwrap();
|
||||||
|
Ok(serde_json::to_string(&conf)?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_or_update_deck_config(&self, conf_json: String) -> Result<i64> {
|
||||||
|
let mut conf: DeckConf = serde_json::from_str(&conf_json)?;
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.transact(None, |col| {
|
||||||
|
col.add_or_update_deck_config(&mut conf)?;
|
||||||
|
Ok(conf.id.0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_deck_config(&self) -> Result<String> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
serde_json::to_string(&col.storage.all_deck_conf()?).map_err(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_deck_config(&self) -> Result<String> {
|
||||||
|
serde_json::to_string(&DeckConf::default()).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_deck_config(&self, dcid: i64) -> Result<()> {
|
||||||
|
self.with_col(|col| col.transact(None, |col| col.remove_deck_config(DeckConfID(dcid))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||||
|
|
|
@ -95,7 +95,6 @@ impl Undoable for UpdateCardUndo {
|
||||||
.storage
|
.storage
|
||||||
.get_card(self.0.id)?
|
.get_card(self.0.id)?
|
||||||
.ok_or_else(|| AnkiError::invalid_input("card disappeared"))?;
|
.ok_or_else(|| AnkiError::invalid_input("card disappeared"))?;
|
||||||
// when called here, update_card should be placing the original content into the redo queue
|
|
||||||
col.update_card(&mut self.0.clone(), ¤t)
|
col.update_card(&mut self.0.clone(), ¤t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,4 +144,12 @@ impl Collection {
|
||||||
// if we cache this in the future, must make sure to invalidate cache when usn bumped in sync.finish()
|
// if we cache this in the future, must make sure to invalidate cache when usn bumped in sync.finish()
|
||||||
self.storage.usn(self.server)
|
self.storage.usn(self.server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ensure_schema_modified(&self) -> Result<()> {
|
||||||
|
if !self.storage.schema_modified()? {
|
||||||
|
Err(AnkiError::SchemaChange)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,10 @@
|
||||||
// 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
|
||||||
|
|
||||||
use crate::decks::DeckID;
|
use crate::decks::DeckID;
|
||||||
use serde::Deserialize as DeTrait;
|
use crate::serde::default_on_invalid;
|
||||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
pub(crate) fn default_on_invalid<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
T: Default + DeTrait<'de>,
|
|
||||||
D: serde::de::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let v: Value = DeTrait::deserialize(deserializer)?;
|
|
||||||
Ok(T::deserialize(v).unwrap_or_default())
|
|
||||||
}
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
243
rslib/src/deckconf.rs
Normal file
243
rslib/src/deckconf.rs
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
collection::Collection,
|
||||||
|
define_newtype,
|
||||||
|
err::{AnkiError, Result},
|
||||||
|
serde::default_on_invalid,
|
||||||
|
timestamp::{TimestampMillis, TimestampSecs},
|
||||||
|
types::Usn,
|
||||||
|
};
|
||||||
|
use serde_aux::field_attributes::{deserialize_bool_from_anything, deserialize_number_from_string};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
use serde_tuple::Serialize_tuple;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
define_newtype!(DeckConfID, i64);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeckConf {
|
||||||
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
|
pub(crate) id: DeckConfID,
|
||||||
|
#[serde(rename = "mod", deserialize_with = "deserialize_number_from_string")]
|
||||||
|
pub(crate) mtime: TimestampSecs,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) usn: Usn,
|
||||||
|
max_taken: i32,
|
||||||
|
autoplay: bool,
|
||||||
|
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||||
|
timer: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
replayq: bool,
|
||||||
|
pub(crate) new: NewConf,
|
||||||
|
pub(crate) rev: RevConf,
|
||||||
|
pub(crate) lapse: LapseConf,
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NewConf {
|
||||||
|
#[serde(default)]
|
||||||
|
bury: bool,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
delays: Vec<f32>,
|
||||||
|
initial_factor: u16,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
ints: NewCardIntervals,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
order: NewCardOrder,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
pub(crate) per_day: u32,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize_tuple, Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
pub struct NewCardIntervals {
|
||||||
|
good: u16,
|
||||||
|
easy: u16,
|
||||||
|
_unused: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NewCardIntervals {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
good: 1,
|
||||||
|
easy: 4,
|
||||||
|
_unused: 7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum NewCardOrder {
|
||||||
|
Random = 0,
|
||||||
|
Due = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NewCardOrder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Due
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hard_factor_default() -> f32 {
|
||||||
|
1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RevConf {
|
||||||
|
#[serde(default)]
|
||||||
|
bury: bool,
|
||||||
|
ease4: f32,
|
||||||
|
ivl_fct: f32,
|
||||||
|
max_ivl: u32,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
pub(crate) per_day: u32,
|
||||||
|
#[serde(default = "hard_factor_default")]
|
||||||
|
hard_factor: f32,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum LeechAction {
|
||||||
|
Suspend = 0,
|
||||||
|
TagOnly = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LapseConf {
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
delays: Vec<f32>,
|
||||||
|
#[serde(deserialize_with = "default_on_invalid")]
|
||||||
|
leech_action: LeechAction,
|
||||||
|
leech_fails: u32,
|
||||||
|
min_int: u32,
|
||||||
|
mult: f32,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LeechAction {
|
||||||
|
fn default() -> Self {
|
||||||
|
LeechAction::Suspend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RevConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
RevConf {
|
||||||
|
bury: false,
|
||||||
|
ease4: 1.3,
|
||||||
|
ivl_fct: 1.0,
|
||||||
|
max_ivl: 36500,
|
||||||
|
per_day: 200,
|
||||||
|
hard_factor: 1.2,
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NewConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
NewConf {
|
||||||
|
bury: false,
|
||||||
|
delays: vec![1.0, 10.0],
|
||||||
|
initial_factor: 2500,
|
||||||
|
ints: NewCardIntervals::default(),
|
||||||
|
order: NewCardOrder::default(),
|
||||||
|
per_day: 20,
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LapseConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
LapseConf {
|
||||||
|
delays: vec![10.0],
|
||||||
|
leech_action: LeechAction::default(),
|
||||||
|
leech_fails: 8,
|
||||||
|
min_int: 1,
|
||||||
|
mult: 0.0,
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DeckConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
DeckConf {
|
||||||
|
id: DeckConfID(0),
|
||||||
|
mtime: TimestampSecs(0),
|
||||||
|
name: "Default".to_string(),
|
||||||
|
usn: Usn(0),
|
||||||
|
max_taken: 60,
|
||||||
|
autoplay: true,
|
||||||
|
timer: false,
|
||||||
|
replayq: true,
|
||||||
|
new: Default::default(),
|
||||||
|
rev: Default::default(),
|
||||||
|
lapse: Default::default(),
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn get_deck_config(&self, dcid: DeckConfID, fallback: bool) -> Result<Option<DeckConf>> {
|
||||||
|
let conf = self.storage.all_deck_conf()?;
|
||||||
|
if let Some(conf) = conf.get(&dcid) {
|
||||||
|
return Ok(Some(conf.clone()));
|
||||||
|
}
|
||||||
|
if fallback {
|
||||||
|
if let Some(conf) = conf.get(&DeckConfID(1)) {
|
||||||
|
return Ok(Some(conf.clone()));
|
||||||
|
}
|
||||||
|
// if even the default deck config is missing, just return the defaults
|
||||||
|
return Ok(Some(DeckConf::default()));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_or_update_deck_config(&self, conf: &mut DeckConf) -> Result<()> {
|
||||||
|
let mut allconf = self.storage.all_deck_conf()?;
|
||||||
|
if conf.id.0 == 0 {
|
||||||
|
conf.id.0 = TimestampMillis::now().0;
|
||||||
|
loop {
|
||||||
|
if !allconf.contains_key(&conf.id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
conf.id.0 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.mtime = TimestampSecs::now();
|
||||||
|
conf.usn = self.usn()?;
|
||||||
|
allconf.insert(conf.id, conf.clone());
|
||||||
|
self.storage.flush_deck_conf(&allconf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_deck_config(&self, dcid: DeckConfID) -> Result<()> {
|
||||||
|
if dcid.0 == 1 {
|
||||||
|
return Err(AnkiError::invalid_input("can't delete default conf"));
|
||||||
|
}
|
||||||
|
self.ensure_schema_modified()?;
|
||||||
|
let mut allconf = self.storage.all_deck_conf()?;
|
||||||
|
allconf.remove(&dcid);
|
||||||
|
self.storage.flush_deck_conf(&allconf)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
use serde_derive::Deserialize;
|
use serde_derive::Deserialize;
|
||||||
|
|
||||||
define_newtype!(DeckID, i64);
|
define_newtype!(DeckID, i64);
|
||||||
define_newtype!(DeckConfID, i64);
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Deck {
|
pub struct Deck {
|
||||||
|
|
|
@ -39,6 +39,9 @@ pub enum AnkiError {
|
||||||
|
|
||||||
#[fail(display = "Close the existing collection first.")]
|
#[fail(display = "Close the existing collection first.")]
|
||||||
CollectionAlreadyOpen,
|
CollectionAlreadyOpen,
|
||||||
|
|
||||||
|
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
|
||||||
|
SchemaChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
// error helpers
|
// error helpers
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub mod card;
|
||||||
pub mod cloze;
|
pub mod cloze;
|
||||||
pub mod collection;
|
pub mod collection;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod deckconf;
|
||||||
pub mod decks;
|
pub mod decks;
|
||||||
pub mod err;
|
pub mod err;
|
||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
|
@ -24,6 +25,7 @@ pub mod notes;
|
||||||
pub mod notetypes;
|
pub mod notetypes;
|
||||||
pub mod sched;
|
pub mod sched;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod serde;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
pub mod template_filters;
|
pub mod template_filters;
|
||||||
|
|
14
rslib/src/serde.rs
Normal file
14
rslib/src/serde.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use serde::Deserialize as DeTrait;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub(crate) fn default_on_invalid<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: Default + DeTrait<'de>,
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let v: Value = DeTrait::deserialize(deserializer)?;
|
||||||
|
Ok(T::deserialize(v).unwrap_or_default())
|
||||||
|
}
|
29
rslib/src/storage/deckconf.rs
Normal file
29
rslib/src/storage/deckconf.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::SqliteStorage;
|
||||||
|
use crate::{
|
||||||
|
deckconf::{DeckConf, DeckConfID},
|
||||||
|
err::{AnkiError, Result},
|
||||||
|
};
|
||||||
|
use rusqlite::{params, NO_PARAMS};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
impl SqliteStorage {
|
||||||
|
pub(crate) fn all_deck_conf(&self) -> Result<HashMap<DeckConfID, DeckConf>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("select dconf from col")?
|
||||||
|
.query_and_then(NO_PARAMS, |row| -> Result<_> {
|
||||||
|
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
|
||||||
|
})?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| AnkiError::invalid_input("no col table"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn flush_deck_conf(&self, conf: &HashMap<DeckConfID, DeckConf>) -> Result<()> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("update col set dconf = ?")?
|
||||||
|
.execute(params![&serde_json::to_string(conf)?])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
mod card;
|
mod card;
|
||||||
|
mod deckconf;
|
||||||
mod sqlite;
|
mod sqlite;
|
||||||
|
|
||||||
pub(crate) use sqlite::SqliteStorage;
|
pub(crate) use sqlite::SqliteStorage;
|
||||||
|
|
|
@ -304,4 +304,11 @@ impl SqliteStorage {
|
||||||
conf.rollover,
|
conf.rollover,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn schema_modified(&self) -> Result<bool> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("select scm > ls from col")?
|
||||||
|
.query_row(NO_PARAMS, |row| row.get(0))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue