Feat/Ignored before card count (#3910)

* GetIgnoredBeforeCount

* get_card_count_with_ignore_before

* Included / total

* Respect search

* Get frontend hooked up

* Fix: Malformed sql and search

* Variable names

* Added: Alert colours

* i18n

* ./check

* Remove console.log

* Fix: Tooltip showing for default value

* Update ftl/core/deck-config.ftl

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* Fix: Multiple backend calls

* Message: (Approximately)

* Fix: Bouncing info message

* Added: Change delay

* Added: ignore_before_updated

* ./check

* Fix typing, camelCase and improve wording

* Temporarily enable the check on startup

---------

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
This commit is contained in:
Luc Mcgrady 2025-04-15 11:21:54 +01:00 committed by GitHub
parent 369dec9319
commit 781a23c6c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 126 additions and 1 deletions

View file

@ -372,6 +372,7 @@ deck-config-good-above-easy = The easy interval should be at least as long as th
deck-config-relearning-steps-above-minimum-interval = The minimum lapse interval should be at least as long as your final relearning step.
deck-config-maximum-answer-secs-above-recommended = Anki can schedule your reviews more efficiently when you keep each question short.
deck-config-too-short-maximum-interval = A maximum interval less than 6 months is not recommended.
deck-config-ignore-before-info = (Approximately) { $included }/{ $totalCards } cards will be used to optimize the FSRS parameters.
## Selecting a deck

View file

@ -23,6 +23,8 @@ service DeckConfigService {
rpc GetDeckConfigsForUpdate(decks.DeckId) returns (DeckConfigsForUpdate);
rpc UpdateDeckConfigs(UpdateDeckConfigsRequest)
returns (collection.OpChanges);
rpc GetIgnoredBeforeCount(GetIgnoredBeforeCountRequest)
returns (GetIgnoredBeforeCountResponse);
}
// Implicitly includes any of the above methods that are not listed in the
@ -33,6 +35,16 @@ message DeckConfigId {
int64 dcid = 1;
}
message GetIgnoredBeforeCountRequest {
string ignore_revlogs_before_date = 1;
string search = 2;
}
message GetIgnoredBeforeCountResponse {
uint64 included = 1;
uint64 total = 2;
}
message DeckConfig {
message Config {
enum NewCardInsertOrder {

View file

@ -652,6 +652,8 @@ exposed_backend_list = [
"evaluate_params",
"get_optimal_retention_parameters",
"simulate_fsrs_review",
# DeckConfigService
"get_ignored_before_count",
]

View file

@ -8,6 +8,7 @@ use crate::deckconfig::DeckConfig;
use crate::deckconfig::DeckConfigId;
use crate::deckconfig::UpdateDeckConfigsRequest;
use crate::error::Result;
use crate::scheduler::fsrs::params::ignore_revlogs_before_date_to_ms;
impl crate::services::DeckConfigService for Collection {
fn add_or_update_deck_config_legacy(
@ -76,6 +77,25 @@ impl crate::services::DeckConfigService for Collection {
) -> Result<anki_proto::collection::OpChanges> {
self.update_deck_configs(input.into()).map(Into::into)
}
fn get_ignored_before_count(
&mut self,
input: anki_proto::deck_config::GetIgnoredBeforeCountRequest,
) -> Result<anki_proto::deck_config::GetIgnoredBeforeCountResponse> {
let timestamp = ignore_revlogs_before_date_to_ms(&input.ignore_revlogs_before_date)?;
let guard = self.search_cards_into_table(
&format!("{} -is:new", input.search),
crate::search::SortMode::NoOrder,
)?;
Ok(anki_proto::deck_config::GetIgnoredBeforeCountResponse {
included: guard
.col
.storage
.get_card_count_with_ignore_before(timestamp)?,
total: guard.cards.try_into().unwrap_or(0),
})
}
}
impl From<DeckConfig> for anki_proto::deck_config::DeckConfig {

View file

@ -33,7 +33,7 @@ use crate::search::SortMode;
pub(crate) type Params = Vec<f32>;
fn ignore_revlogs_before_date_to_ms(
pub(crate) fn ignore_revlogs_before_date_to_ms(
ignore_revlogs_before_date: &String,
) -> Result<TimestampMillis> {
Ok(match ignore_revlogs_before_date {

View file

@ -0,0 +1,5 @@
SELECT COUNT(DISTINCT cid)
FROM revlog
WHERE id > ?
AND type == 0
AND cid IN search_cids

View file

@ -732,6 +732,20 @@ impl super::SqliteStorage {
Ok(())
}
pub(crate) fn get_card_count_with_ignore_before(
&self,
ignore_before: TimestampMillis,
) -> Result<u64> {
Ok(self
.db
.prepare(include_str!("get_ignored_before_count.sql"))?
.query(params![ignore_before.0])?
.next()
.unwrap()
.unwrap()
.get(0)?)
}
#[cfg(test)]
pub(crate) fn get_all_cards(&self) -> Vec<Card> {
self.db

View file

@ -21,6 +21,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import SpinBoxRow from "./SpinBoxRow.svelte";
import DateInput from "./DateInput.svelte";
import Warning from "./Warning.svelte";
import { getIgnoredBeforeCount } from "@generated/backend";
import type { GetIgnoredBeforeCountResponse } from "@generated/anki/deck_config_pb";
export let state: DeckOptionsState;
export let api: Record<string, never>;
@ -91,6 +93,68 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? tr.deckConfigTooShortMaximumInterval()
: "";
let ignoreRevlogsBeforeCount: GetIgnoredBeforeCountResponse | null = null;
let lastIgnoreRevlogsBeforeDate = "";
function updateIgnoreRevlogsBeforeCount(ignoreRevlogsBeforeDate: string) {
if (lastIgnoreRevlogsBeforeDate == ignoreRevlogsBeforeDate) {
return;
}
if (
cutoffUpdatedSinceLoad &&
ignoreRevlogsBeforeDate &&
ignoreRevlogsBeforeDate != "1970-01-01"
) {
lastIgnoreRevlogsBeforeDate = ignoreRevlogsBeforeDate;
getIgnoredBeforeCount({
search:
$config.paramSearch ||
`preset:"${state.getCurrentNameForSearch()}" -is:suspended`,
ignoreRevlogsBeforeDate,
}).then((resp) => {
ignoreRevlogsBeforeCount = resp;
});
} else {
ignoreRevlogsBeforeCount = null;
}
cutoffUpdatedSinceLoad = true;
}
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
// Running the card count check on startup is inefficient. After users have had a few months
// to notice + update (e.g. from ~Oct 2025), we should change this to false.
let cutoffUpdatedSinceLoad = true;
const IGNORE_REVLOG_COUNT_DELAY_MS = 1000;
$: {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
updateIgnoreRevlogsBeforeCount($config.ignoreRevlogsBeforeDate);
}, IGNORE_REVLOG_COUNT_DELAY_MS);
}
let ignoreRevlogsBeforeWarningClass = "alert-warning";
$: if (ignoreRevlogsBeforeCount) {
// If there is less than a tenth of reviews included
if (
Number(ignoreRevlogsBeforeCount.included) /
Number(ignoreRevlogsBeforeCount.total) <
0.1
) {
ignoreRevlogsBeforeWarningClass = "alert-danger";
} else if (
ignoreRevlogsBeforeCount.included != ignoreRevlogsBeforeCount.total
) {
ignoreRevlogsBeforeWarningClass = "alert-warning";
} else {
ignoreRevlogsBeforeWarningClass = "alert-info";
}
}
$: ignoreRevlogsBeforeWarning = ignoreRevlogsBeforeCount
? tr.deckConfigIgnoreBeforeInfo({
included: ignoreRevlogsBeforeCount.included.toString(),
totalCards: ignoreRevlogsBeforeCount.total.toString(),
})
: "";
let modal: Modal;
let carousel: Carousel;
@ -249,6 +313,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SettingTitle>
</DateInput>
</Item>
<Item>
<Warning
warning={ignoreRevlogsBeforeWarning}
className={ignoreRevlogsBeforeWarningClass}
></Warning>
</Item>
{/if}
<Item>