mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Feat/Desired retention warning improvements (#3995)
* Feat/90% desired retention warning * Update ftl/core/deck-config.ftl * show on newly enabled * Show warning on focus * Never hide warning * Display relative change * Add: Separate warning for too long and short * Revert unchanged text changes * interval -> workload * Remove dead code * fsrs-rs/@L-M-Sherlock's workload calculation * Added: delay * CONSTANT_CASE * Fix: optimized state * Removed "Processing" * Remove dead code * 1 digit precision * bump fsrs-rs * typo * Apply suggestions from code review Co-authored-by: Damien Elmes <dae@users.noreply.github.com> * Improve rounding * improve comment * rounding <1% * decrease rounding precision * bump ts-fsrs * use actual cost values * ./check * typo * include relearning * change factor wording * simplify sql * ./check * Apply suggestions from code review Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Fix: missing search_cids * @dae's style patch * Fix: Doesn't update on arrow keys change * force two lines * center two lines --------- Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
This commit is contained in:
parent
1e6d12b830
commit
f29bcb743b
10 changed files with 207 additions and 22 deletions
|
@ -470,11 +470,12 @@ deck-config-compute-optimal-retention-tooltip4 =
|
||||||
willing to invest more study time to achieve it. Setting your desired retention lower than the minimum
|
willing to invest more study time to achieve it. Setting your desired retention lower than the minimum
|
||||||
is not recommended, as it will lead to a higher workload, because of the high forgetting rate.
|
is not recommended, as it will lead to a higher workload, because of the high forgetting rate.
|
||||||
deck-config-please-save-your-changes-first = Please save your changes first.
|
deck-config-please-save-your-changes-first = Please save your changes first.
|
||||||
deck-config-a-100-day-interval =
|
deck-config-workload-factor-change = Approximate workload: {$factor}x
|
||||||
{ $days ->
|
(compared to {$previousDR}% desired retention)
|
||||||
[one] A 100 day interval will become { $days } day.
|
deck-config-workload-factor-unchanged = The higher this value, the more frequently cards will be shown to you.
|
||||||
*[other] A 100 day interval will become { $days } days.
|
deck-config-desired-retention-too-low = Your desired retention is very low, which can lead to very long intervals.
|
||||||
}
|
deck-config-desired-retention-too-high = Your desired retention is very high, which can lead to very short intervals.
|
||||||
|
|
||||||
deck-config-percent-of-reviews =
|
deck-config-percent-of-reviews =
|
||||||
{ $reviews ->
|
{ $reviews ->
|
||||||
[one] { $pct }% of { $reviews } review
|
[one] { $pct }% of { $reviews } review
|
||||||
|
@ -512,6 +513,12 @@ deck-config-fsrs-simulator-radio-memorized = Memorized
|
||||||
|
|
||||||
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
|
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
|
||||||
|
|
||||||
|
deck-config-a-100-day-interval =
|
||||||
|
{ $days ->
|
||||||
|
[one] A 100 day interval will become { $days } day.
|
||||||
|
*[other] A 100 day interval will become { $days } days.
|
||||||
|
}
|
||||||
|
|
||||||
deck-config-fsrs-simulator-y-axis-title-time = Review Time/Day
|
deck-config-fsrs-simulator-y-axis-title-time = Review Time/Day
|
||||||
deck-config-fsrs-simulator-y-axis-title-count = Review Count/Day
|
deck-config-fsrs-simulator-y-axis-title-count = Review Count/Day
|
||||||
deck-config-fsrs-simulator-y-axis-title-memorized = Memorized Total
|
deck-config-fsrs-simulator-y-axis-title-memorized = Memorized Total
|
||||||
|
|
|
@ -25,6 +25,8 @@ service DeckConfigService {
|
||||||
returns (collection.OpChanges);
|
returns (collection.OpChanges);
|
||||||
rpc GetIgnoredBeforeCount(GetIgnoredBeforeCountRequest)
|
rpc GetIgnoredBeforeCount(GetIgnoredBeforeCountRequest)
|
||||||
returns (GetIgnoredBeforeCountResponse);
|
returns (GetIgnoredBeforeCountResponse);
|
||||||
|
rpc GetRetentionWorkload(GetRetentionWorkloadRequest)
|
||||||
|
returns (GetRetentionWorkloadResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implicitly includes any of the above methods that are not listed in the
|
// Implicitly includes any of the above methods that are not listed in the
|
||||||
|
@ -35,6 +37,17 @@ message DeckConfigId {
|
||||||
int64 dcid = 1;
|
int64 dcid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetRetentionWorkloadRequest {
|
||||||
|
repeated float w = 1;
|
||||||
|
string search = 2;
|
||||||
|
float before = 3;
|
||||||
|
float after = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRetentionWorkloadResponse {
|
||||||
|
float factor = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetIgnoredBeforeCountRequest {
|
message GetIgnoredBeforeCountRequest {
|
||||||
string ignore_revlogs_before_date = 1;
|
string ignore_revlogs_before_date = 1;
|
||||||
string search = 2;
|
string search = 2;
|
||||||
|
|
|
@ -659,6 +659,7 @@ exposed_backend_list = [
|
||||||
"simulate_fsrs_review",
|
"simulate_fsrs_review",
|
||||||
# DeckConfigService
|
# DeckConfigService
|
||||||
"get_ignored_before_count",
|
"get_ignored_before_count",
|
||||||
|
"get_retention_workload",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,40 @@ impl crate::services::DeckConfigService for Collection {
|
||||||
total: guard.cards.try_into().unwrap_or(0),
|
total: guard.cards.try_into().unwrap_or(0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_retention_workload(
|
||||||
|
&mut self,
|
||||||
|
input: anki_proto::deck_config::GetRetentionWorkloadRequest,
|
||||||
|
) -> Result<anki_proto::deck_config::GetRetentionWorkloadResponse> {
|
||||||
|
const LEARN_SPAN: usize = 1000;
|
||||||
|
|
||||||
|
let guard =
|
||||||
|
self.search_cards_into_table(&input.search, crate::search::SortMode::NoOrder)?;
|
||||||
|
let (pass_cost, fail_cost, learn_cost) = guard.col.storage.get_costs_for_retention()?;
|
||||||
|
|
||||||
|
let before = fsrs::expected_workload(
|
||||||
|
&input.w,
|
||||||
|
input.before,
|
||||||
|
LEARN_SPAN,
|
||||||
|
pass_cost,
|
||||||
|
fail_cost,
|
||||||
|
0.,
|
||||||
|
input.before,
|
||||||
|
)? + learn_cost;
|
||||||
|
let after = fsrs::expected_workload(
|
||||||
|
&input.w,
|
||||||
|
input.after,
|
||||||
|
LEARN_SPAN,
|
||||||
|
pass_cost,
|
||||||
|
fail_cost,
|
||||||
|
0.,
|
||||||
|
input.after,
|
||||||
|
)? + learn_cost;
|
||||||
|
|
||||||
|
Ok(anki_proto::deck_config::GetRetentionWorkloadResponse {
|
||||||
|
factor: after / before,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeckConfig> for anki_proto::deck_config::DeckConfig {
|
impl From<DeckConfig> for anki_proto::deck_config::DeckConfig {
|
||||||
|
|
49
rslib/src/storage/card/get_costs_for_retention.sql
Normal file
49
rslib/src/storage/card/get_costs_for_retention.sql
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
WITH searched_revlogs AS (
|
||||||
|
SELECT *
|
||||||
|
FROM revlog
|
||||||
|
WHERE ease > 0
|
||||||
|
AND cid IN search_cids
|
||||||
|
ORDER BY id DESC -- Use the last 10_000 reviews
|
||||||
|
LIMIT 10000
|
||||||
|
), average_pass AS (
|
||||||
|
SELECT AVG(time)
|
||||||
|
FROM searched_revlogs
|
||||||
|
WHERE ease > 1
|
||||||
|
),
|
||||||
|
lapse_count AS (
|
||||||
|
SELECT COUNT(time) AS lapse_count
|
||||||
|
FROM searched_revlogs
|
||||||
|
WHERE ease = 1
|
||||||
|
AND type = 1
|
||||||
|
),
|
||||||
|
fail_sum AS (
|
||||||
|
SELECT SUM(time) AS total_fail_time
|
||||||
|
FROM searched_revlogs
|
||||||
|
WHERE (
|
||||||
|
ease = 1
|
||||||
|
AND type = 1
|
||||||
|
)
|
||||||
|
OR type = 2
|
||||||
|
),
|
||||||
|
-- (sum(Relearning) + sum(Lapses)) / count(Lapses)
|
||||||
|
average_fail AS (
|
||||||
|
SELECT total_fail_time * 1.0 / NULLIF(lapse_count, 0) AS avg_fail_time
|
||||||
|
FROM fail_sum,
|
||||||
|
lapse_count
|
||||||
|
),
|
||||||
|
-- Can lead to cards with partial learn histories skewing the time
|
||||||
|
summed_learns AS (
|
||||||
|
SELECT cid,
|
||||||
|
SUM(time) AS total_time
|
||||||
|
FROM searched_revlogs
|
||||||
|
WHERE searched_revlogs.type = 0
|
||||||
|
GROUP BY cid
|
||||||
|
),
|
||||||
|
average_learn AS (
|
||||||
|
SELECT AVG(total_time) AS avg_learn_time
|
||||||
|
FROM summed_learns
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM average_pass,
|
||||||
|
average_fail,
|
||||||
|
average_learn;
|
|
@ -747,6 +747,20 @@ impl super::SqliteStorage {
|
||||||
.get(0)?)
|
.get(0)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_costs_for_retention(&self) -> Result<(f32, f32, f32)> {
|
||||||
|
let mut statement = self
|
||||||
|
.db
|
||||||
|
.prepare(include_str!("get_costs_for_retention.sql"))?;
|
||||||
|
let mut query = statement.query(params![])?;
|
||||||
|
let row = query.next()?.unwrap();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
row.get(0).unwrap_or(7000.),
|
||||||
|
row.get(1).unwrap_or(23_000.),
|
||||||
|
row.get(2).unwrap_or(30_000.),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn get_all_cards(&self) -> Vec<Card> {
|
pub(crate) fn get_all_cards(&self) -> Vec<Card> {
|
||||||
self.db
|
self.db
|
||||||
|
|
|
@ -23,7 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let percentage = false;
|
export let percentage = false;
|
||||||
|
|
||||||
let input: HTMLInputElement;
|
let input: HTMLInputElement;
|
||||||
let focused = false;
|
export let focused = false;
|
||||||
let multiplier: number;
|
let multiplier: number;
|
||||||
$: multiplier = percentage ? 100 : 1;
|
$: multiplier = percentage ? 100 : 1;
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
value={stringValue}
|
value={stringValue}
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
on:blur={update}
|
on:blur={update}
|
||||||
|
on:change={update}
|
||||||
on:input={onInput}
|
on:input={onInput}
|
||||||
on:focusin={() => (focused = true)}
|
on:focusin={() => (focused = true)}
|
||||||
on:focusout={() => (focused = false)}
|
on:focusout={() => (focused = false)}
|
||||||
|
|
|
@ -11,6 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import {
|
import {
|
||||||
computeFsrsParams,
|
computeFsrsParams,
|
||||||
evaluateParams,
|
evaluateParams,
|
||||||
|
getRetentionWorkload,
|
||||||
setWantsAbort,
|
setWantsAbort,
|
||||||
} from "@generated/backend";
|
} from "@generated/backend";
|
||||||
import * as tr from "@generated/ftl";
|
import * as tr from "@generated/ftl";
|
||||||
|
@ -26,11 +27,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import ParamsInputRow from "./ParamsInputRow.svelte";
|
import ParamsInputRow from "./ParamsInputRow.svelte";
|
||||||
import ParamsSearchRow from "./ParamsSearchRow.svelte";
|
import ParamsSearchRow from "./ParamsSearchRow.svelte";
|
||||||
import SimulatorModal from "./SimulatorModal.svelte";
|
import SimulatorModal from "./SimulatorModal.svelte";
|
||||||
import { UpdateDeckConfigsMode } from "@generated/anki/deck_config_pb";
|
import {
|
||||||
|
GetRetentionWorkloadRequest,
|
||||||
|
UpdateDeckConfigsMode,
|
||||||
|
} from "@generated/anki/deck_config_pb";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let openHelpModal: (String) => void;
|
export let openHelpModal: (String) => void;
|
||||||
export let onPresetChange: () => void;
|
export let onPresetChange: () => void;
|
||||||
|
export let newlyEnabled = false;
|
||||||
|
|
||||||
const config = state.currentConfig;
|
const config = state.currentConfig;
|
||||||
const defaults = state.defaults;
|
const defaults = state.defaults;
|
||||||
|
@ -39,6 +44,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
$: lastOptimizationWarning =
|
$: lastOptimizationWarning =
|
||||||
$daysSinceLastOptimization > 30 ? tr.deckConfigTimeToOptimize() : "";
|
$daysSinceLastOptimization > 30 ? tr.deckConfigTimeToOptimize() : "";
|
||||||
|
let desiredRetentionFocused = false;
|
||||||
|
let desiredRetentionEverFocused = false;
|
||||||
|
let optimized = false;
|
||||||
|
const startingDesiredRetention = $config.desiredRetention.toFixed(2);
|
||||||
|
$: if (desiredRetentionFocused) {
|
||||||
|
desiredRetentionEverFocused = true;
|
||||||
|
}
|
||||||
|
$: showDesiredRetentionTooltip =
|
||||||
|
newlyEnabled || desiredRetentionEverFocused || optimized;
|
||||||
|
|
||||||
let computeParamsProgress: ComputeParamsProgress | undefined;
|
let computeParamsProgress: ComputeParamsProgress | undefined;
|
||||||
let computingParams = false;
|
let computingParams = false;
|
||||||
|
@ -47,10 +61,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
$: computing = computingParams || checkingParams;
|
$: computing = computingParams || checkingParams;
|
||||||
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
|
$: defaultparamSearch = `preset:"${state.getCurrentNameForSearch()}" -is:suspended`;
|
||||||
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
|
$: roundedRetention = Number($config.desiredRetention.toFixed(2));
|
||||||
$: desiredRetentionWarning = getRetentionWarning(
|
$: desiredRetentionWarning = getRetentionLongShortWarning(roundedRetention);
|
||||||
roundedRetention,
|
|
||||||
fsrsParams($config),
|
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
);
|
const WORKLOAD_UPDATE_DELAY_MS = 100;
|
||||||
|
|
||||||
|
let desiredRetentionChangeInfo = "";
|
||||||
|
$: {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (showDesiredRetentionTooltip) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
getRetentionChangeInfo(roundedRetention, fsrsParams($config));
|
||||||
|
}, WORKLOAD_UPDATE_DELAY_MS);
|
||||||
|
} else {
|
||||||
|
desiredRetentionChangeInfo = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
|
$: retentionWarningClass = getRetentionWarningClass(roundedRetention);
|
||||||
|
|
||||||
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
|
$: newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
|
||||||
|
@ -67,23 +94,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
reviewOrder: $config.reviewOrder,
|
reviewOrder: $config.reviewOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
function getRetentionWarning(retention: number, params: number[]): string {
|
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;
|
||||||
const decay = params.length > 20 ? -params[20] : -0.5; // default decay for FSRS-4.5 and FSRS-5
|
const DESIRED_RETENTION_HIGH_THRESHOLD = 0.95;
|
||||||
const factor = 0.9 ** (1 / decay) - 1;
|
|
||||||
const stability = 100;
|
function getRetentionLongShortWarning(retention: number) {
|
||||||
const days = Math.round(
|
if (retention < DESIRED_RETENTION_LOW_THRESHOLD) {
|
||||||
(stability / factor) * (Math.pow(retention, 1 / decay) - 1),
|
return tr.deckConfigDesiredRetentionTooLow();
|
||||||
);
|
} else if (retention > DESIRED_RETENTION_HIGH_THRESHOLD) {
|
||||||
if (days === 100) {
|
return tr.deckConfigDesiredRetentionTooHigh();
|
||||||
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return tr.deckConfigA100DayInterval({ days });
|
}
|
||||||
|
|
||||||
|
async function getRetentionChangeInfo(retention: number, params: number[]) {
|
||||||
|
if (+startingDesiredRetention == roundedRetention) {
|
||||||
|
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorUnchanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const request = new GetRetentionWorkloadRequest({
|
||||||
|
w: params,
|
||||||
|
search: defaultparamSearch,
|
||||||
|
before: +startingDesiredRetention,
|
||||||
|
after: retention,
|
||||||
|
});
|
||||||
|
const resp = await getRetentionWorkload(request);
|
||||||
|
desiredRetentionChangeInfo = tr.deckConfigWorkloadFactorChange({
|
||||||
|
factor: resp.factor.toFixed(2),
|
||||||
|
previousDr: (+startingDesiredRetention * 100).toString(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRetentionWarningClass(retention: number): string {
|
function getRetentionWarningClass(retention: number): string {
|
||||||
if (retention < 0.7 || retention > 0.97) {
|
if (retention < 0.7 || retention > 0.97) {
|
||||||
return "alert-danger";
|
return "alert-danger";
|
||||||
} else if (retention < 0.8 || retention > 0.95) {
|
} else if (
|
||||||
|
retention < DESIRED_RETENTION_LOW_THRESHOLD ||
|
||||||
|
retention > DESIRED_RETENTION_HIGH_THRESHOLD
|
||||||
|
) {
|
||||||
return "alert-warning";
|
return "alert-warning";
|
||||||
} else {
|
} else {
|
||||||
return "alert-info";
|
return "alert-info";
|
||||||
|
@ -146,6 +194,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
setTimeout(() => alert(msg), 200);
|
setTimeout(() => alert(msg), 200);
|
||||||
} else {
|
} else {
|
||||||
$config.fsrsParams6 = resp.params;
|
$config.fsrsParams6 = resp.params;
|
||||||
|
optimized = true;
|
||||||
}
|
}
|
||||||
if (computeParamsProgress) {
|
if (computeParamsProgress) {
|
||||||
computeParamsProgress.current = computeParamsProgress.total;
|
computeParamsProgress.current = computeParamsProgress.total;
|
||||||
|
@ -237,12 +286,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
min={0.7}
|
min={0.7}
|
||||||
max={0.99}
|
max={0.99}
|
||||||
percentage={true}
|
percentage={true}
|
||||||
|
bind:focused={desiredRetentionFocused}
|
||||||
>
|
>
|
||||||
<SettingTitle on:click={() => openHelpModal("desiredRetention")}>
|
<SettingTitle on:click={() => openHelpModal("desiredRetention")}>
|
||||||
{tr.deckConfigDesiredRetention()}
|
{tr.deckConfigDesiredRetention()}
|
||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
</SpinBoxFloatRow>
|
</SpinBoxFloatRow>
|
||||||
|
|
||||||
|
<Warning warning={desiredRetentionChangeInfo} className={"alert-info two-line"} />
|
||||||
<Warning warning={desiredRetentionWarning} className={retentionWarningClass} />
|
<Warning warning={desiredRetentionWarning} className={retentionWarningClass} />
|
||||||
|
|
||||||
<div class="ms-1 me-1">
|
<div class="ms-1 me-1">
|
||||||
|
@ -331,4 +382,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
.btn {
|
.btn {
|
||||||
margin-bottom: 0.375rem;
|
margin-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.two-line) {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
min-height: calc(2ch + 30px);
|
||||||
|
box-sizing: content-box;
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,6 +25,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let onPresetChange: () => void;
|
export let onPresetChange: () => void;
|
||||||
|
|
||||||
const fsrs = state.fsrs;
|
const fsrs = state.fsrs;
|
||||||
|
let newlyEnabled = false;
|
||||||
|
$: if (!$fsrs) {
|
||||||
|
newlyEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
fsrs: {
|
fsrs: {
|
||||||
|
@ -94,6 +98,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{#if $fsrs}
|
{#if $fsrs}
|
||||||
<FsrsOptions
|
<FsrsOptions
|
||||||
{state}
|
{state}
|
||||||
|
{newlyEnabled}
|
||||||
openHelpModal={(key) =>
|
openHelpModal={(key) =>
|
||||||
openHelpModal(Object.keys(settings).indexOf(key))}
|
openHelpModal(Object.keys(settings).indexOf(key))}
|
||||||
{onPresetChange}
|
{onPresetChange}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
export let max = 9999;
|
export let max = 9999;
|
||||||
export let step = 0.01;
|
export let step = 0.01;
|
||||||
export let percentage = false;
|
export let percentage = false;
|
||||||
|
export let focused = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row --cols={13}>
|
<Row --cols={13}>
|
||||||
|
@ -23,7 +24,7 @@
|
||||||
</Col>
|
</Col>
|
||||||
<Col --col-size={6} breakpoint="xs">
|
<Col --col-size={6} breakpoint="xs">
|
||||||
<ConfigInput>
|
<ConfigInput>
|
||||||
<SpinBox bind:value {min} {max} {step} {percentage} />
|
<SpinBox bind:value {min} {max} {step} {percentage} bind:focused />
|
||||||
<RevertButton slot="revert" bind:value {defaultValue} />
|
<RevertButton slot="revert" bind:value {defaultValue} />
|
||||||
</ConfigInput>
|
</ConfigInput>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
Loading…
Reference in a new issue