mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
store config in separate DB table
- mtime is tracked on each key individually, which will allow merging of config changes when syncing in the future - added col.(get|set|remove)_config() - in order to support existing code that was mutating returned values (eg col.conf["something"]["another"] = 5), the returned list/dict will be automatically wrapped so that when the value is dropped, it will save the mutated item back to the DB if it's changed. Code that is fetching lists/dicts from the config like so: col.conf["foo"]["bar"] = baz col.setMod() will continue to work in most case, but should be gradually updated to: conf = col.get_config("foo") conf["bar"] = baz col.set_config("foo", conf)
This commit is contained in:
parent
8b76098bc7
commit
676f4e74a8
24 changed files with 539 additions and 125 deletions
|
@ -61,6 +61,10 @@ message BackendInput {
|
|||
string canonify_tags = 49;
|
||||
Empty all_tags = 50;
|
||||
int32 get_changed_tags = 51;
|
||||
string get_config_json = 52;
|
||||
SetConfigJson set_config_json = 53;
|
||||
string set_all_config = 54;
|
||||
Empty get_all_config = 55;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +109,10 @@ message BackendOutput {
|
|||
CanonifyTagsOut canonify_tags = 49;
|
||||
AllTagsOut all_tags = 50;
|
||||
GetChangedTagsOut get_changed_tags = 51;
|
||||
string get_config_json = 52;
|
||||
Empty set_config_json = 53;
|
||||
Empty set_all_config = 54;
|
||||
string get_all_config = 55;
|
||||
|
||||
BackendError error = 2047;
|
||||
}
|
||||
|
@ -454,3 +462,11 @@ message CanonifyTagsOut {
|
|||
string tags = 1;
|
||||
bool tag_list_changed = 2;
|
||||
}
|
||||
|
||||
message SetConfigJson {
|
||||
string key = 1;
|
||||
oneof op {
|
||||
string val = 2;
|
||||
Empty remove = 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pprint
|
||||
import random
|
||||
|
@ -22,6 +21,7 @@ import anki.latex # sets up hook
|
|||
import anki.template
|
||||
from anki import hooks
|
||||
from anki.cards import Card
|
||||
from anki.config import ConfigManager
|
||||
from anki.consts import *
|
||||
from anki.dbproxy import DBProxy
|
||||
from anki.decks import DeckManager
|
||||
|
@ -45,25 +45,6 @@ from anki.utils import (
|
|||
stripHTMLMedia,
|
||||
)
|
||||
|
||||
defaultConf = {
|
||||
# review options
|
||||
"activeDecks": [1],
|
||||
"curDeck": 1,
|
||||
"newSpread": NEW_CARDS_DISTRIBUTE,
|
||||
"collapseTime": 1200,
|
||||
"timeLim": 0,
|
||||
"estTimes": True,
|
||||
"dueCounts": True,
|
||||
# other config
|
||||
"curModel": None,
|
||||
"nextPos": 1,
|
||||
"sortType": "noteFld",
|
||||
"sortBackwards": False,
|
||||
"addToCur": True, # add new to currently selected deck?
|
||||
"dayLearnFirst": False,
|
||||
"schedVer": 1,
|
||||
}
|
||||
|
||||
|
||||
# this is initialized by storage.Collection
|
||||
class _Collection:
|
||||
|
@ -75,7 +56,6 @@ class _Collection:
|
|||
dty: bool # no longer used
|
||||
_usn: int
|
||||
ls: int
|
||||
conf: Dict[str, Any]
|
||||
_undo: List[Any]
|
||||
|
||||
def __init__(
|
||||
|
@ -97,6 +77,7 @@ class _Collection:
|
|||
self.models = ModelManager(self)
|
||||
self.decks = DeckManager(self)
|
||||
self.tags = TagManager(self)
|
||||
self.conf = ConfigManager(self)
|
||||
self.load()
|
||||
if not self.crt:
|
||||
d = datetime.datetime.today()
|
||||
|
@ -179,23 +160,21 @@ class _Collection:
|
|||
self.dty, # no longer used
|
||||
self._usn,
|
||||
self.ls,
|
||||
conf,
|
||||
models,
|
||||
decks,
|
||||
) = self.db.first(
|
||||
"""
|
||||
select crt, mod, scm, dty, usn, ls,
|
||||
conf, models, decks from col"""
|
||||
models, decks from col"""
|
||||
)
|
||||
self.conf = json.loads(conf)
|
||||
self.models.load(models)
|
||||
self.decks.load(decks)
|
||||
|
||||
def setMod(self) -> None:
|
||||
"""Mark DB modified.
|
||||
|
||||
DB operations and the deck/tag/model managers do this automatically, so this
|
||||
is only necessary if you modify properties of this object or the conf dict."""
|
||||
DB operations and the deck/model managers do this automatically, so this
|
||||
is only necessary if you modify properties of this object."""
|
||||
self.db.mod = True
|
||||
|
||||
def flush(self, mod: Optional[int] = None) -> None:
|
||||
|
@ -203,14 +182,13 @@ is only necessary if you modify properties of this object or the conf dict."""
|
|||
self.mod = intTime(1000) if mod is None else mod
|
||||
self.db.execute(
|
||||
"""update col set
|
||||
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
|
||||
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?""",
|
||||
self.crt,
|
||||
self.mod,
|
||||
self.scm,
|
||||
self.dty,
|
||||
self._usn,
|
||||
self.ls,
|
||||
json.dumps(self.conf),
|
||||
)
|
||||
|
||||
def flush_all_changes(self, mod: Optional[int] = None):
|
||||
|
@ -651,6 +629,23 @@ where c.nid = n.id and c.id in %s group by nid"""
|
|||
findCards = find_cards
|
||||
findNotes = find_notes
|
||||
|
||||
# Config
|
||||
##########################################################################
|
||||
|
||||
def get_config(self, key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return self.conf.get_immutable(key)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def set_config(self, key: str, val: Any):
|
||||
self.setMod()
|
||||
self.conf.set(key, val)
|
||||
|
||||
def remove_config(self, key):
|
||||
self.setMod()
|
||||
self.conf.remove(key)
|
||||
|
||||
# Stats
|
||||
##########################################################################
|
||||
|
||||
|
|
121
pylib/anki/config.py
Normal file
121
pylib/anki/config.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"""
|
||||
Config handling
|
||||
|
||||
- To set a config value, use col.set_config(key, val).
|
||||
- To get a config value, use col.get_config(key, default=None). In
|
||||
the case of lists and dictionaries, any changes you make to the returned
|
||||
value will not be saved unless you call set_config().
|
||||
- To remove a config value, use col.remove_config(key).
|
||||
|
||||
For legacy reasons, the config is also exposed as a dict interface
|
||||
as col.conf. To support old code that was mutating inner values,
|
||||
using col.conf["key"] needs to wrap lists and dicts when returning them.
|
||||
As this is less efficient, please use the col.*_config() API in new code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import weakref
|
||||
from typing import Any
|
||||
|
||||
import anki
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, col: anki.storage._Collection):
|
||||
self.col = col.weakref()
|
||||
|
||||
def get_immutable(self, key: str) -> Any:
|
||||
s = self.col.backend.get_config_json(key)
|
||||
if not s:
|
||||
raise KeyError
|
||||
return json.loads(s)
|
||||
|
||||
def set(self, key: str, val: Any) -> None:
|
||||
self.col.backend.set_config_json(key, val)
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
self.col.backend.remove_config(key)
|
||||
|
||||
# Legacy dict interface
|
||||
#########################
|
||||
|
||||
def __getitem__(self, key):
|
||||
val = self.get_immutable(key)
|
||||
if isinstance(val, list):
|
||||
print(
|
||||
f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()"
|
||||
)
|
||||
return WrappedList(weakref.ref(self), key, val)
|
||||
elif isinstance(val, dict):
|
||||
print(
|
||||
f"conf key {key} should be fetched with col.get_config(), and saved with col.set_config()"
|
||||
)
|
||||
return WrappedDict(weakref.ref(self), key, val)
|
||||
else:
|
||||
return val
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.set(key, value)
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default):
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
return self[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
self.get_immutable(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.remove(key)
|
||||
|
||||
|
||||
# Tracking changes to mutable objects
|
||||
#########################################
|
||||
# Because we previously allowed mutation of the conf
|
||||
# structure directly, to allow col.conf["foo"]["bar"] = xx
|
||||
# to continue to function, we apply changes as the object
|
||||
# is dropped.
|
||||
|
||||
|
||||
class WrappedList(list):
|
||||
def __init__(self, conf, key, val):
|
||||
self.key = key
|
||||
self.conf = conf
|
||||
self.orig = copy.deepcopy(val)
|
||||
super().__init__(val)
|
||||
|
||||
def __del__(self):
|
||||
cur = list(self)
|
||||
conf = self.conf()
|
||||
if conf and self.orig != cur:
|
||||
conf[self.key] = cur
|
||||
|
||||
|
||||
class WrappedDict(dict):
|
||||
def __init__(self, conf, key, val):
|
||||
self.key = key
|
||||
self.conf = conf
|
||||
self.orig = copy.deepcopy(val)
|
||||
super().__init__(val)
|
||||
|
||||
def __del__(self):
|
||||
cur = dict(self)
|
||||
conf = self.conf()
|
||||
if conf and self.orig != cur:
|
||||
conf[self.key] = cur
|
|
@ -524,8 +524,8 @@ class DeckManager:
|
|||
#############################################################
|
||||
|
||||
def active(self) -> Any:
|
||||
"The currrently active dids. Make sure to copy before modifying."
|
||||
return self.col.conf["activeDecks"]
|
||||
"The currrently active dids."
|
||||
return self.col.get_config("activeDecks", [1])
|
||||
|
||||
def selected(self) -> Any:
|
||||
"The currently selected did."
|
||||
|
|
|
@ -577,6 +577,32 @@ class RustBackend:
|
|||
).get_changed_tags.tags
|
||||
)
|
||||
|
||||
def get_config_json(self, key: str) -> str:
|
||||
return self._run_command(pb.BackendInput(get_config_json=key)).get_config_json
|
||||
|
||||
def set_config_json(self, key: str, val: Any):
|
||||
self._run_command(
|
||||
pb.BackendInput(
|
||||
set_config_json=pb.SetConfigJson(key=key, val=json.dumps(val))
|
||||
)
|
||||
)
|
||||
|
||||
def remove_config(self, key: str):
|
||||
self._run_command(
|
||||
pb.BackendInput(
|
||||
set_config_json=pb.SetConfigJson(key=key, remove=pb.Empty())
|
||||
)
|
||||
)
|
||||
|
||||
def get_all_config(self) -> Dict[str, Any]:
|
||||
jstr = self._run_command(
|
||||
pb.BackendInput(get_all_config=pb.Empty())
|
||||
).get_all_config
|
||||
return json.loads(jstr)
|
||||
|
||||
def set_all_config(self, conf: Dict[str, Any]):
|
||||
self._run_command(pb.BackendInput(set_all_config=json.dumps(conf)))
|
||||
|
||||
|
||||
def translate_string_in(
|
||||
key: TR, **kwargs: Union[str, int, float]
|
||||
|
|
|
@ -6,10 +6,9 @@ import json
|
|||
import os
|
||||
import weakref
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from anki.collection import _Collection
|
||||
from anki.consts import *
|
||||
from anki.dbproxy import DBProxy
|
||||
from anki.lang import _
|
||||
from anki.media import media_paths_from_col_path
|
||||
|
@ -74,26 +73,18 @@ def Collection(
|
|||
|
||||
|
||||
def initial_db_setup(db: DBProxy) -> None:
|
||||
db.begin()
|
||||
_addColVars(db, *_getColVars(db))
|
||||
|
||||
|
||||
def _getColVars(db: DBProxy) -> Tuple[Any, Dict[str, Any]]:
|
||||
import anki.collection
|
||||
import anki.decks
|
||||
|
||||
db.begin()
|
||||
|
||||
g = copy.deepcopy(anki.decks.defaultDeck)
|
||||
g["id"] = 1
|
||||
g["name"] = _("Default")
|
||||
g["conf"] = 1
|
||||
g["mod"] = intTime()
|
||||
return g, anki.collection.defaultConf.copy()
|
||||
|
||||
|
||||
def _addColVars(db: DBProxy, g: Dict[str, Any], c: Dict[str, Any]) -> None:
|
||||
db.execute(
|
||||
"""
|
||||
update col set conf = ?, decks = ?""",
|
||||
json.dumps(c),
|
||||
update col set decks = ?""",
|
||||
json.dumps({"1": g}),
|
||||
)
|
||||
|
|
|
@ -449,11 +449,11 @@ from notes where %s"""
|
|||
# Col config
|
||||
##########################################################################
|
||||
|
||||
def getConf(self) -> Any:
|
||||
return self.col.conf
|
||||
def getConf(self) -> Dict[str, Any]:
|
||||
return self.col.backend.get_all_config()
|
||||
|
||||
def mergeConf(self, conf) -> None:
|
||||
self.col.conf = conf
|
||||
def mergeConf(self, conf: Dict[str, Any]) -> None:
|
||||
self.col.backend.set_all_config(conf)
|
||||
|
||||
|
||||
# HTTP syncing tools
|
||||
|
|
|
@ -80,7 +80,7 @@ class DataModel(QAbstractTableModel):
|
|||
self.browser = browser
|
||||
self.col = browser.col
|
||||
self.sortKey = None
|
||||
self.activeCols = self.col.conf.get(
|
||||
self.activeCols = self.col.get_config(
|
||||
"activeCols", ["noteFld", "template", "cardDue", "deck"]
|
||||
)
|
||||
self.cards: Sequence[int] = []
|
||||
|
@ -1121,7 +1121,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
|
||||
def _favTree(self, root) -> None:
|
||||
assert self.col
|
||||
saved = self.col.conf.get("savedFilters", {})
|
||||
saved = self.col.get_config("savedFilters", {})
|
||||
for name, filt in sorted(saved.items()):
|
||||
item = SidebarItem(
|
||||
name,
|
||||
|
@ -1360,7 +1360,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
ml = MenuList()
|
||||
# make sure exists
|
||||
if "savedFilters" not in self.col.conf:
|
||||
self.col.conf["savedFilters"] = {}
|
||||
self.col.set_config("savedFilters", {})
|
||||
|
||||
ml.addSeparator()
|
||||
|
||||
|
@ -1369,7 +1369,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
else:
|
||||
ml.addItem(_("Save Current Filter..."), self._onSaveFilter)
|
||||
|
||||
saved = self.col.conf["savedFilters"]
|
||||
saved = self.col.get_config("savedFilters")
|
||||
if not saved:
|
||||
return ml
|
||||
|
||||
|
@ -1384,8 +1384,9 @@ QTableView {{ gridline-color: {grid} }}
|
|||
if not name:
|
||||
return
|
||||
filt = self.form.searchEdit.lineEdit().text()
|
||||
self.col.conf["savedFilters"][name] = filt
|
||||
self.col.setMod()
|
||||
conf = self.col.get_config("savedFilters")
|
||||
conf[name] = filt
|
||||
self.col.save_config("savedFilters", conf)
|
||||
self.maybeRefreshSidebar()
|
||||
|
||||
def _onRemoveFilter(self):
|
||||
|
@ -1399,7 +1400,7 @@ QTableView {{ gridline-color: {grid} }}
|
|||
# returns name if found
|
||||
def _currentFilterIsSaved(self):
|
||||
filt = self.form.searchEdit.lineEdit().text()
|
||||
for k, v in self.col.conf["savedFilters"].items():
|
||||
for k, v in self.col.get_config("savedFilters").items():
|
||||
if filt == v:
|
||||
return k
|
||||
return None
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from operator import itemgetter
|
||||
|
||||
from typing import Union
|
||||
|
||||
import aqt
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::backend::dbproxy::db_command_bytes;
|
||||
use crate::backend_proto::backend_input::Value;
|
||||
use crate::backend_proto::{
|
||||
AddOrUpdateDeckConfigIn, BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn,
|
||||
};
|
||||
|
@ -34,7 +33,9 @@ use crate::{backend_proto as pb, log};
|
|||
use fluent::FluentValue;
|
||||
use futures::future::{AbortHandle, Abortable};
|
||||
use log::error;
|
||||
use pb::backend_input::Value;
|
||||
use prost::Message;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::path::PathBuf;
|
||||
|
@ -291,6 +292,17 @@ impl Backend {
|
|||
Value::AllTags(_) => OValue::AllTags(self.all_tags()?),
|
||||
Value::RegisterTags(input) => OValue::RegisterTags(self.register_tags(input)?),
|
||||
Value::GetChangedTags(usn) => OValue::GetChangedTags(self.get_changed_tags(usn)?),
|
||||
Value::GetConfigJson(key) => OValue::GetConfigJson(self.get_config_json(&key)?),
|
||||
Value::SetConfigJson(input) => OValue::SetConfigJson({
|
||||
self.set_config_json(input)?;
|
||||
pb::Empty {}
|
||||
}),
|
||||
|
||||
Value::SetAllConfig(input) => OValue::SetConfigJson({
|
||||
self.set_all_config(&input)?;
|
||||
pb::Empty {}
|
||||
}),
|
||||
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -400,8 +412,8 @@ impl Backend {
|
|||
|
||||
fn sched_timing_today(&self, input: pb::SchedTimingTodayIn) -> pb::SchedTimingTodayOut {
|
||||
let today = sched_timing_today(
|
||||
input.created_secs as i64,
|
||||
input.now_secs as i64,
|
||||
TimestampSecs(input.created_secs),
|
||||
TimestampSecs(input.now_secs),
|
||||
input.created_mins_west.map(|v| v.val),
|
||||
input.now_mins_west.map(|v| v.val),
|
||||
input.rollover_hour.map(|v| v.val as i8),
|
||||
|
@ -783,6 +795,52 @@ impl Backend {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_config_json(&self, key: &str) -> Result<String> {
|
||||
self.with_col(|col| {
|
||||
let val: Option<JsonValue> = col.get_config_optional(key);
|
||||
match val {
|
||||
None => Ok("".to_string()),
|
||||
Some(val) => Ok(serde_json::to_string(&val)?),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_config_json(&self, input: pb::SetConfigJson) -> Result<()> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
if let Some(op) = input.op {
|
||||
match op {
|
||||
pb::set_config_json::Op::Val(val) => {
|
||||
// ensure it's a well-formed object
|
||||
let val: JsonValue = serde_json::from_str(&val)?;
|
||||
col.set_config(&input.key, &val)
|
||||
}
|
||||
pb::set_config_json::Op::Remove(_) => col.remove_config(&input.key),
|
||||
}
|
||||
} else {
|
||||
Err(AnkiError::invalid_input("no op received"))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_all_config(&self, conf: &str) -> Result<()> {
|
||||
let val: HashMap<String, JsonValue> = serde_json::from_str(conf)?;
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
col.storage
|
||||
.set_all_config(val, col.usn()?, TimestampSecs::now())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_all_config(&self) -> Result<String> {
|
||||
self.with_col(|col| {
|
||||
let conf = col.storage.get_all_config()?;
|
||||
serde_json::to_string(&conf).map_err(Into::into)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||
|
|
|
@ -6,7 +6,11 @@ use crate::i18n::I18n;
|
|||
use crate::log::Logger;
|
||||
use crate::timestamp::TimestampSecs;
|
||||
use crate::types::Usn;
|
||||
use crate::{sched::cutoff::SchedTimingToday, storage::SqliteStorage, undo::UndoManager};
|
||||
use crate::{
|
||||
sched::cutoff::{sched_timing_today, SchedTimingToday},
|
||||
storage::SqliteStorage,
|
||||
undo::UndoManager,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn open_collection<P: Into<PathBuf>>(
|
||||
|
@ -141,8 +145,24 @@ impl Collection {
|
|||
return Ok(*timing);
|
||||
}
|
||||
}
|
||||
self.state.timing_today = Some(self.storage.timing_today(self.server)?);
|
||||
Ok(self.state.timing_today.clone().unwrap())
|
||||
|
||||
let local_offset = if self.server {
|
||||
self.get_local_mins_west()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let timing = sched_timing_today(
|
||||
self.storage.creation_stamp()?,
|
||||
TimestampSecs::now(),
|
||||
self.get_creation_mins_west(),
|
||||
local_offset,
|
||||
self.get_rollover(),
|
||||
);
|
||||
|
||||
self.state.timing_today = Some(timing);
|
||||
|
||||
Ok(timing)
|
||||
}
|
||||
|
||||
pub(crate) fn usn(&self) -> Result<Usn> {
|
||||
|
|
|
@ -1,26 +1,111 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::decks::DeckID;
|
||||
use crate::serde::default_on_invalid;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use crate::err::Result;
|
||||
use crate::timestamp::TimestampSecs;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_derive::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Config {
|
||||
#[serde(
|
||||
rename = "curDeck",
|
||||
deserialize_with = "deserialize_number_from_string"
|
||||
)]
|
||||
pub(crate) current_deck_id: DeckID,
|
||||
pub(crate) rollover: Option<i8>,
|
||||
pub(crate) creation_offset: Option<i32>,
|
||||
pub(crate) local_offset: Option<i32>,
|
||||
#[serde(rename = "sortType", deserialize_with = "default_on_invalid")]
|
||||
pub(crate) browser_sort_kind: SortKind,
|
||||
#[serde(rename = "sortBackwards", deserialize_with = "default_on_invalid")]
|
||||
pub(crate) browser_sort_reverse: bool,
|
||||
pub(crate) fn schema11_config_as_string() -> String {
|
||||
let obj = json!({
|
||||
"activeDecks": [1],
|
||||
"curDeck": 1,
|
||||
"newSpread": 0,
|
||||
"collapseTime": 1200,
|
||||
"timeLim": 0,
|
||||
"estTimes": true,
|
||||
"dueCounts": true,
|
||||
"curModel": null,
|
||||
"nextPos": 1,
|
||||
"sortType": "noteFld",
|
||||
"sortBackwards": false,
|
||||
"addToCur": true,
|
||||
"dayLearnFirst": false,
|
||||
"schedVer": 1,
|
||||
});
|
||||
serde_json::to_string(&obj).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) enum ConfigKey {
|
||||
BrowserSortKind,
|
||||
BrowserSortReverse,
|
||||
CurrentDeckID,
|
||||
CreationOffset,
|
||||
Rollover,
|
||||
LocalOffset,
|
||||
}
|
||||
|
||||
impl From<ConfigKey> for &'static str {
|
||||
fn from(c: ConfigKey) -> Self {
|
||||
match c {
|
||||
ConfigKey::BrowserSortKind => "sortType",
|
||||
ConfigKey::BrowserSortReverse => "sortBackwards",
|
||||
ConfigKey::CurrentDeckID => "curDeck",
|
||||
ConfigKey::CreationOffset => "creationOffset",
|
||||
ConfigKey::Rollover => "rollover",
|
||||
ConfigKey::LocalOffset => "localOffset",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
{
|
||||
match self.storage.get_config_value(key.into()) {
|
||||
Ok(Some(val)) => val,
|
||||
_ => 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()
|
||||
}
|
||||
|
||||
pub(crate) fn set_config<T: Serialize>(&self, key: &str, val: &T) -> Result<()> {
|
||||
self.storage
|
||||
.set_config_value(key, val, self.usn()?, TimestampSecs::now())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_config(&self, key: &str) -> Result<()> {
|
||||
self.storage.remove_config(key)
|
||||
}
|
||||
|
||||
pub(crate) fn get_browser_sort_kind(&self) -> SortKind {
|
||||
self.get_config_default(ConfigKey::BrowserSortKind)
|
||||
}
|
||||
|
||||
pub(crate) fn get_browser_sort_reverse(&self) -> bool {
|
||||
self.get_config_default(ConfigKey::BrowserSortReverse)
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_deck_id(&self) -> DeckID {
|
||||
self.get_config_optional(ConfigKey::CurrentDeckID)
|
||||
.unwrap_or(DeckID(1))
|
||||
}
|
||||
|
||||
pub(crate) fn get_creation_mins_west(&self) -> Option<i32> {
|
||||
self.get_config_optional(ConfigKey::CreationOffset)
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_mins_west(&self) -> Option<i32> {
|
||||
self.get_config_optional(ConfigKey::LocalOffset)
|
||||
}
|
||||
|
||||
pub(crate) fn get_rollover(&self) -> Option<i8> {
|
||||
self.get_config_optional(ConfigKey::Rollover)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, PartialEq, Debug)]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::timestamp::TimestampSecs;
|
||||
use chrono::{Date, Duration, FixedOffset, Local, TimeZone};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
|
@ -138,25 +139,25 @@ fn sched_timing_today_v2_legacy(
|
|||
|
||||
/// Based on provided input, get timing info from the relevant function.
|
||||
pub(crate) fn sched_timing_today(
|
||||
created_secs: i64,
|
||||
now_secs: i64,
|
||||
created_secs: TimestampSecs,
|
||||
now_secs: TimestampSecs,
|
||||
created_mins_west: Option<i32>,
|
||||
now_mins_west: Option<i32>,
|
||||
rollover_hour: Option<i8>,
|
||||
) -> SchedTimingToday {
|
||||
let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now_secs));
|
||||
let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now_secs.0));
|
||||
match (rollover_hour, created_mins_west) {
|
||||
(None, _) => {
|
||||
// if rollover unset, v1 scheduler
|
||||
sched_timing_today_v1(created_secs, now_secs)
|
||||
sched_timing_today_v1(created_secs.0, now_secs.0)
|
||||
}
|
||||
(Some(roll), None) => {
|
||||
// if creationOffset unset, v2 scheduler with legacy cutoff handling
|
||||
sched_timing_today_v2_legacy(created_secs, roll, now_secs, now_west)
|
||||
sched_timing_today_v2_legacy(created_secs.0, roll, now_secs.0, now_west)
|
||||
}
|
||||
(Some(roll), Some(crt_west)) => {
|
||||
// v2 scheduler, new cutoff handling
|
||||
sched_timing_today_v2_new(created_secs, crt_west, now_secs, now_west, roll)
|
||||
sched_timing_today_v2_new(created_secs.0, crt_west, now_secs.0, now_west, roll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@ pub(crate) fn search_cards<'a, 'b>(
|
|||
match order {
|
||||
SortMode::NoOrder => (),
|
||||
SortMode::FromConfig => {
|
||||
let conf = req.storage.all_config()?;
|
||||
prepare_sort(req, &conf.browser_sort_kind)?;
|
||||
let kind = req.get_browser_sort_kind();
|
||||
prepare_sort(req, &kind)?;
|
||||
sql.push_str(" order by ");
|
||||
write_order(&mut sql, &conf.browser_sort_kind, conf.browser_sort_reverse)?;
|
||||
write_order(&mut sql, &kind, req.get_browser_sort_reverse())?;
|
||||
}
|
||||
SortMode::Builtin { kind, reverse } => {
|
||||
prepare_sort(req, &kind)?;
|
||||
|
|
|
@ -231,9 +231,9 @@ impl SqlWriter<'_> {
|
|||
.map(|(_, v)| v)
|
||||
.collect();
|
||||
let dids_with_children = if deck == "current" {
|
||||
let config = self.col.storage.all_config()?;
|
||||
let mut dids_with_children = vec![config.current_deck_id];
|
||||
let current = get_deck(&all_decks, config.current_deck_id)
|
||||
let current_id = self.col.get_current_deck_id();
|
||||
let mut dids_with_children = vec![current_id];
|
||||
let current = get_deck(&all_decks, current_id)
|
||||
.ok_or_else(|| AnkiError::invalid_input("invalid current deck"))?;
|
||||
for child_did in child_ids(&all_decks, ¤t.name) {
|
||||
dids_with_children.push(child_did);
|
||||
|
|
4
rslib/src/storage/config/add.sql
Normal file
4
rslib/src/storage/config/add.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
insert
|
||||
or replace into config (key, usn, mtime_secs, val)
|
||||
values
|
||||
(?, ?, ?, ?)
|
5
rslib/src/storage/config/get.sql
Normal file
5
rslib/src/storage/config/get.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
select
|
||||
val
|
||||
from config
|
||||
where
|
||||
key = ?
|
88
rslib/src/storage/config/mod.rs
Normal file
88
rslib/src/storage/config/mod.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
// 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::{err::Result, timestamp::TimestampSecs, types::Usn};
|
||||
use rusqlite::{params, NO_PARAMS};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl SqliteStorage {
|
||||
pub(crate) fn set_config_value<T: Serialize>(
|
||||
&self,
|
||||
key: &str,
|
||||
val: &T,
|
||||
usn: Usn,
|
||||
mtime: TimestampSecs,
|
||||
) -> Result<()> {
|
||||
let json = serde_json::to_string(val)?;
|
||||
self.db
|
||||
.prepare_cached(include_str!("add.sql"))?
|
||||
.execute(params![key, usn, mtime, json])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_config(&self, key: &str) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached("delete from config where key=?")?
|
||||
.execute(&[key])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_config_value<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
|
||||
self.db
|
||||
.prepare_cached(include_str!("get.sql"))?
|
||||
.query_and_then(&[key], |row| {
|
||||
serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into)
|
||||
})?
|
||||
.next()
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_config(&self) -> Result<HashMap<String, Value>> {
|
||||
self.db
|
||||
.prepare("select key, val from config")?
|
||||
.query_and_then(NO_PARAMS, |row| {
|
||||
let val: Value = serde_json::from_str(row.get_raw(1).as_str()?)?;
|
||||
Ok((row.get::<usize, String>(0)?, val))
|
||||
})?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_all_config(
|
||||
&self,
|
||||
conf: HashMap<String, Value>,
|
||||
usn: Usn,
|
||||
mtime: TimestampSecs,
|
||||
) -> Result<()> {
|
||||
self.db.execute("delete from config", NO_PARAMS)?;
|
||||
for (key, val) in conf.iter() {
|
||||
self.set_config_value(key, val, usn, mtime)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Upgrading/downgrading
|
||||
|
||||
pub(super) fn upgrade_config_to_schema14(&self) -> Result<()> {
|
||||
let conf = self
|
||||
.db
|
||||
.query_row_and_then("select conf from col", NO_PARAMS, |row| {
|
||||
let conf: Result<HashMap<String, Value>> =
|
||||
serde_json::from_str(row.get_raw(0).as_str()?).map_err(Into::into);
|
||||
conf
|
||||
})?;
|
||||
self.set_all_config(conf, Usn(0), TimestampSecs(0))?;
|
||||
self.db.execute_batch("update col set conf=''")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn downgrade_config_from_schema14(&self) -> Result<()> {
|
||||
let allconf = self.get_all_config()?;
|
||||
self.db
|
||||
.execute("update col set conf=?", &[serde_json::to_string(&allconf)?])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod card;
|
||||
mod config;
|
||||
mod deckconf;
|
||||
mod sqlite;
|
||||
mod tag;
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::schema11_config_as_string;
|
||||
use crate::decks::DeckID;
|
||||
use crate::err::Result;
|
||||
use crate::err::{AnkiError, DBErrorKind};
|
||||
use crate::notetypes::NoteTypeID;
|
||||
use crate::timestamp::{TimestampMillis, TimestampSecs};
|
||||
use crate::{
|
||||
decks::Deck,
|
||||
i18n::I18n,
|
||||
notetypes::NoteType,
|
||||
sched::cutoff::{sched_timing_today, SchedTimingToday},
|
||||
text::without_combining,
|
||||
types::Usn,
|
||||
};
|
||||
use crate::{decks::Deck, i18n::I18n, notetypes::NoteType, text::without_combining, types::Usn};
|
||||
use regex::Regex;
|
||||
use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
|
||||
use std::cmp::Ordering;
|
||||
|
@ -23,7 +16,7 @@ use unicase::UniCase;
|
|||
|
||||
const SCHEMA_MIN_VERSION: u8 = 11;
|
||||
const SCHEMA_STARTING_VERSION: u8 = 11;
|
||||
const SCHEMA_MAX_VERSION: u8 = 13;
|
||||
const SCHEMA_MAX_VERSION: u8 = 14;
|
||||
|
||||
fn unicase_compare(s1: &str, s2: &str) -> Ordering {
|
||||
UniCase::new(s1).cmp(&UniCase::new(s2))
|
||||
|
@ -182,8 +175,12 @@ impl SqliteStorage {
|
|||
db.execute_batch(include_str!("schema11.sql"))?;
|
||||
// start at schema 11, then upgrade below
|
||||
db.execute(
|
||||
"update col set crt=?, ver=?",
|
||||
params![TimestampSecs::now(), SCHEMA_STARTING_VERSION],
|
||||
"update col set crt=?, ver=?, conf=?",
|
||||
params![
|
||||
TimestampSecs::now(),
|
||||
SCHEMA_STARTING_VERSION,
|
||||
&schema11_config_as_string()
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
|
@ -290,13 +287,6 @@ impl SqliteStorage {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn all_config(&self) -> Result<Config> {
|
||||
self.db
|
||||
.query_row_and_then("select conf from col", NO_PARAMS, |row| -> Result<_> {
|
||||
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn all_note_types(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
|
||||
let mut stmt = self.db.prepare("select models from col")?;
|
||||
let note_types = stmt
|
||||
|
@ -313,21 +303,11 @@ impl SqliteStorage {
|
|||
Ok(note_types)
|
||||
}
|
||||
|
||||
pub(crate) fn timing_today(&self, server: bool) -> Result<SchedTimingToday> {
|
||||
let crt: i64 = self
|
||||
.db
|
||||
pub(crate) fn creation_stamp(&self) -> Result<TimestampSecs> {
|
||||
self.db
|
||||
.prepare_cached("select crt from col")?
|
||||
.query_row(NO_PARAMS, |row| row.get(0))?;
|
||||
let conf = self.all_config()?;
|
||||
let now_offset = if server { conf.local_offset } else { None };
|
||||
|
||||
Ok(sched_timing_today(
|
||||
crt,
|
||||
TimestampSecs::now().0,
|
||||
conf.creation_offset,
|
||||
now_offset,
|
||||
conf.rollover,
|
||||
))
|
||||
.query_row(NO_PARAMS, |row| row.get(0))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn schema_modified(&self) -> Result<bool> {
|
||||
|
|
|
@ -58,7 +58,7 @@ impl SqliteStorage {
|
|||
|
||||
// Upgrading/downgrading
|
||||
|
||||
pub(super) fn upgrade_tags_to_schema12(&self) -> Result<()> {
|
||||
pub(super) fn upgrade_tags_to_schema13(&self) -> Result<()> {
|
||||
let tags = self
|
||||
.db
|
||||
.query_row_and_then("select tags from col", NO_PARAMS, |row| {
|
||||
|
|
|
@ -14,14 +14,24 @@ impl SqliteStorage {
|
|||
if ver < 13 {
|
||||
self.db
|
||||
.execute_batch(include_str!("schema13_upgrade.sql"))?;
|
||||
self.upgrade_tags_to_schema12()?;
|
||||
self.upgrade_tags_to_schema13()?;
|
||||
}
|
||||
if ver < 14 {
|
||||
self.db
|
||||
.execute_batch(include_str!("schema14_upgrade.sql"))?;
|
||||
self.upgrade_config_to_schema14()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn downgrade_to_schema_11(&self) -> Result<()> {
|
||||
self.begin_trx()?;
|
||||
|
||||
self.downgrade_config_from_schema14()?;
|
||||
self.db
|
||||
.execute_batch(include_str!("schema14_downgrade.sql"))?;
|
||||
|
||||
self.downgrade_tags_from_schema13()?;
|
||||
self.db
|
||||
.execute_batch(include_str!("schema13_downgrade.sql"))?;
|
||||
|
|
4
rslib/src/storage/upgrades/schema14_downgrade.sql
Normal file
4
rslib/src/storage/upgrades/schema14_downgrade.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
drop table config;
|
||||
update col
|
||||
set
|
||||
ver = 13;
|
9
rslib/src/storage/upgrades/schema14_upgrade.sql
Normal file
9
rslib/src/storage/upgrades/schema14_upgrade.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
create table config (
|
||||
key text not null primary key,
|
||||
usn integer not null,
|
||||
mtime_secs integer not null,
|
||||
val text not null
|
||||
) without rowid;
|
||||
update col
|
||||
set
|
||||
ver = 14;
|
Loading…
Reference in a new issue