more experimental updates to deck config screen
- try out bootstrap modals - they're not perfect, but let's see how they go for now. Won't be hard to switch to bridge commands if required. - handle adding/renaming/removing - add a class to manage the state
This commit is contained in:
parent
a6ed8e90ce
commit
c3fc07ac20
@ -5,10 +5,11 @@ load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte", "svelte_check")
|
||||
load("//ts:esbuild.bzl", "esbuild")
|
||||
load("//ts:vendor.bzl", "copy_bootstrap_icons")
|
||||
load("//ts:compile_sass.bzl", "compile_sass")
|
||||
load("@npm//jest-cli:index.bzl", "jest_test")
|
||||
|
||||
compile_sass(
|
||||
group = "base_css",
|
||||
srcs = ["deckconfig-base.scss"],
|
||||
group = "base_css",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//ts/sass:base_lib",
|
||||
@ -24,6 +25,9 @@ svelte_names = [f.replace(".svelte", "") for f in svelte_files]
|
||||
compile_svelte(
|
||||
name = "svelte",
|
||||
srcs = svelte_files,
|
||||
deps = [
|
||||
"@npm//@types/bootstrap",
|
||||
],
|
||||
)
|
||||
|
||||
copy_bootstrap_icons(
|
||||
@ -50,12 +54,16 @@ ts_library(
|
||||
"icons.ts",
|
||||
"lib.ts",
|
||||
"steps.ts",
|
||||
"textInputModal.ts",
|
||||
],
|
||||
module_name = "deckconfig",
|
||||
deps = [
|
||||
"TextInputModal",
|
||||
"//ts:image_module_support",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"@npm//lodash-es",
|
||||
"@npm//svelte",
|
||||
],
|
||||
)
|
||||
|
||||
@ -112,5 +120,40 @@ svelte_check(
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
"*.svelte",
|
||||
]),
|
||||
]) + [
|
||||
"@npm//@types/bootstrap",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
srcs = glob(["*.test.ts"]),
|
||||
tsconfig = "//ts:tsconfig.json",
|
||||
deps = [
|
||||
":lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"@npm//@types/jest",
|
||||
],
|
||||
)
|
||||
|
||||
jest_test(
|
||||
name = "test",
|
||||
args = [
|
||||
"--no-cache",
|
||||
"--no-watchman",
|
||||
"--ci",
|
||||
"--colors",
|
||||
"--config",
|
||||
"$(location //ts:jest.config.js)",
|
||||
],
|
||||
data = [
|
||||
":test_lib",
|
||||
"//ts:jest.config.js",
|
||||
"@npm//protobufjs",
|
||||
],
|
||||
target_compatible_with = select({
|
||||
"@platforms//os:osx": [],
|
||||
"@platforms//os:linux": [],
|
||||
"//conditions:default": ["@platforms//os:linux"],
|
||||
}),
|
||||
)
|
||||
|
@ -3,14 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import NewOptions from "./NewOptions.svelte";
|
||||
import ReviewOptions from "./ReviewOptions.svelte";
|
||||
import LapseOptions from "./LapseOptions.svelte";
|
||||
import GeneralOptions from "./GeneralOptions.svelte";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let config: pb.BackendProto.DeckConfig.Config;
|
||||
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||
export let state: DeckConfigState;
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -24,8 +23,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<NewOptions bind:config {defaults} />
|
||||
<ReviewOptions bind:config {defaults} />
|
||||
<LapseOptions bind:config {defaults} />
|
||||
<GeneralOptions bind:config {defaults} />
|
||||
<NewOptions {state} />
|
||||
<ReviewOptions {state} />
|
||||
<LapseOptions {state} />
|
||||
<GeneralOptions {state} />
|
||||
</div>
|
||||
|
@ -4,16 +4,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "anki/i18n";
|
||||
import type { DeckConfigId, ConfigWithCount } from "./lib";
|
||||
import type { DeckConfigState, ConfigListEntry } from "./lib";
|
||||
import OptionsDropdown from "./OptionsDropdown.svelte";
|
||||
|
||||
export let allConfig: ConfigWithCount[];
|
||||
export let selectedConfigId: DeckConfigId;
|
||||
export let state: DeckConfigState;
|
||||
let configList = state.configList;
|
||||
|
||||
function configLabel(config: ConfigWithCount): string {
|
||||
const name = config.config.name;
|
||||
const count = tr.deckConfigUsedByDecks({ decks: config.useCount });
|
||||
return `${name} (${count})`;
|
||||
function configLabel(entry: ConfigListEntry): string {
|
||||
const count = tr.deckConfigUsedByDecks({ decks: entry.useCount });
|
||||
return `${entry.name} (${count})`;
|
||||
}
|
||||
|
||||
function myblur(this: HTMLSelectElement) {
|
||||
state.setCurrentIndex(parseInt(this.value));
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -47,12 +50,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<select bind:value={selectedConfigId} class="form-select">
|
||||
{#each allConfig as config}
|
||||
<option value={config.config.id}>{configLabel(config)}</option>
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select class="form-select" on:change={myblur}>
|
||||
{#each $configList as entry}
|
||||
<option value={entry.idx} selected={entry.current}>
|
||||
{configLabel(entry)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<OptionsDropdown />
|
||||
<OptionsDropdown {state} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,24 +3,12 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import ConfigSelector from "./ConfigSelector.svelte";
|
||||
import ConfigEditor from "./ConfigEditor.svelte";
|
||||
import * as tr from "anki/i18n";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let state: DeckConfigState;
|
||||
|
||||
let selectedConfigId = state.selectedConfigId;
|
||||
|
||||
let selectedConfig: pb.BackendProto.DeckConfig.Config;
|
||||
$: {
|
||||
selectedConfig = (
|
||||
state.allConfigs.find((e) => e.config.id == selectedConfigId)?.config ??
|
||||
state.allConfigs[0].config
|
||||
).config as pb.BackendProto.DeckConfig.Config;
|
||||
}
|
||||
let defaults = state.defaults;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -38,12 +26,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="modal">
|
||||
<!-- filled in later-->
|
||||
</div>
|
||||
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<div><b>{tr.actionsOptionsFor({ val: state.deckName })}</b></div>
|
||||
<div><b>{tr.actionsOptionsFor({ val: state.currentDeck.name })}</b></div>
|
||||
|
||||
<ConfigSelector allConfig={state.allConfigs} bind:selectedConfigId />
|
||||
<ConfigSelector {state} />
|
||||
|
||||
<ConfigEditor config={selectedConfig} {defaults} />
|
||||
<ConfigEditor {state} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,13 +3,14 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import * as tr from "anki/i18n";
|
||||
import SpinBox from "./SpinBox.svelte";
|
||||
import CheckBox from "./CheckBox.svelte";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let config: pb.BackendProto.DeckConfig.Config;
|
||||
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||
export let state: DeckConfigState;
|
||||
let config = state.currentConfig;
|
||||
let defaults = state.defaults;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@ -21,23 +22,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={30}
|
||||
max={600}
|
||||
defaultValue={defaults.capAnswerTimeToSecs}
|
||||
bind:value={config.capAnswerTimeToSecs} />
|
||||
bind:value={$config.capAnswerTimeToSecs} />
|
||||
|
||||
<CheckBox
|
||||
label="Answer timer"
|
||||
subLabel={tr.schedulingShowAnswerTimer()}
|
||||
defaultValue={defaults.showTimer}
|
||||
bind:value={config.showTimer} />
|
||||
bind:value={$config.showTimer} />
|
||||
|
||||
<CheckBox
|
||||
label="Autoplay"
|
||||
subLabel="Don't play audio automatically"
|
||||
defaultValue={defaults.disableAutoplay}
|
||||
bind:value={config.disableAutoplay} />
|
||||
bind:value={$config.disableAutoplay} />
|
||||
|
||||
<CheckBox
|
||||
label="Question Audio"
|
||||
subLabel={tr.schedulingAlwaysIncludeQuestionSideWhenReplaying()}
|
||||
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
||||
bind:value={config.skipQuestionWhenReplayingAnswer} />
|
||||
bind:value={$config.skipQuestionWhenReplayingAnswer} />
|
||||
</div>
|
||||
|
@ -3,15 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import * as tr from "anki/i18n";
|
||||
import SpinBox from "./SpinBox.svelte";
|
||||
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||
import StepsInput from "./StepsInput.svelte";
|
||||
import EnumSelector from "./EnumSelector.svelte";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let config: pb.BackendProto.DeckConfig.Config;
|
||||
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||
export let state: DeckConfigState;
|
||||
let config = state.currentConfig;
|
||||
let defaults = state.defaults;
|
||||
|
||||
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
||||
</script>
|
||||
@ -23,8 +24,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
label="Relearning steps"
|
||||
subLabel="Relearning steps, separated by spaces."
|
||||
defaultValue={defaults.relearnSteps}
|
||||
value={config.relearnSteps}
|
||||
on:changed={(evt) => (config.relearnSteps = evt.detail.value)} />
|
||||
value={$config.relearnSteps}
|
||||
on:changed={(evt) => ($config.relearnSteps = evt.detail.value)} />
|
||||
|
||||
<SpinBoxFloat
|
||||
label={tr.schedulingNewInterval()}
|
||||
@ -32,27 +33,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaults.lapseMultiplier}
|
||||
value={config.lapseMultiplier}
|
||||
on:changed={(evt) => (config.lapseMultiplier = evt.detail.value)} />
|
||||
value={$config.lapseMultiplier}
|
||||
on:changed={(evt) => ($config.lapseMultiplier = evt.detail.value)} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingMinimumInterval()}
|
||||
subLabel="The minimum new interval a lapsed card will be given after relearning."
|
||||
min={1}
|
||||
defaultValue={defaults.minimumLapseInterval}
|
||||
bind:value={config.minimumLapseInterval} />
|
||||
bind:value={$config.minimumLapseInterval} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingLeechThreshold()}
|
||||
subLabel="Number of times Again needs to be pressed on a review card to make it a leech."
|
||||
min={1}
|
||||
defaultValue={defaults.leechThreshold}
|
||||
bind:value={config.leechThreshold} />
|
||||
bind:value={$config.leechThreshold} />
|
||||
|
||||
<EnumSelector
|
||||
label={tr.schedulingLeechAction()}
|
||||
subLabel=""
|
||||
choices={leechChoices}
|
||||
defaultValue={defaults.leechAction}
|
||||
bind:value={config.leechAction} />
|
||||
bind:value={$config.leechAction} />
|
||||
</div>
|
||||
|
@ -3,16 +3,17 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import * as tr from "anki/i18n";
|
||||
import SpinBox from "./SpinBox.svelte";
|
||||
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||
import CheckBox from "./CheckBox.svelte";
|
||||
import StepsInput from "./StepsInput.svelte";
|
||||
import EnumSelector from "./EnumSelector.svelte";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let config: pb.BackendProto.DeckConfig.Config;
|
||||
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||
export let state: DeckConfigState;
|
||||
let config = state.currentConfig;
|
||||
let defaults = state.defaults;
|
||||
|
||||
const newOrderChoices = [
|
||||
tr.schedulingShowNewCardsInOrderAdded(),
|
||||
@ -21,15 +22,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
let stepsExceedGraduatingInterval: boolean;
|
||||
$: {
|
||||
const lastLearnStepInDays = config.learnSteps.length
|
||||
? config.learnSteps[config.learnSteps.length - 1] / 60 / 24
|
||||
const lastLearnStepInDays = $config.learnSteps.length
|
||||
? $config.learnSteps[$config.learnSteps.length - 1] / 60 / 24
|
||||
: 0;
|
||||
stepsExceedGraduatingInterval =
|
||||
lastLearnStepInDays > config.graduatingIntervalGood;
|
||||
lastLearnStepInDays > $config.graduatingIntervalGood;
|
||||
}
|
||||
|
||||
let goodExceedsEasy: boolean;
|
||||
$: goodExceedsEasy = config.graduatingIntervalGood > config.graduatingIntervalEasy;
|
||||
$: goodExceedsEasy =
|
||||
$config.graduatingIntervalGood > $config.graduatingIntervalEasy;
|
||||
|
||||
// fixme: change impl; support warning messages
|
||||
$: newCardsGreaterThanParent = $config.newPerDay > state.currentDeck.parentNewLimit;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@ -40,36 +44,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
subLabel="Learning steps, separated by spaces."
|
||||
warn={stepsExceedGraduatingInterval}
|
||||
defaultValue={defaults.learnSteps}
|
||||
value={config.learnSteps}
|
||||
on:changed={(evt) => (config.learnSteps = evt.detail.value)} />
|
||||
value={$config.learnSteps}
|
||||
on:changed={(evt) => ($config.learnSteps = evt.detail.value)} />
|
||||
|
||||
<EnumSelector
|
||||
label={tr.schedulingOrder()}
|
||||
subLabel=""
|
||||
choices={newOrderChoices}
|
||||
defaultValue={defaults.newCardOrder}
|
||||
bind:value={config.newCardOrder} />
|
||||
bind:value={$config.newCardOrder} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingNewCardsday()}
|
||||
subLabel="The maximum number of new cards to introduce in a day."
|
||||
min={0}
|
||||
warn={newCardsGreaterThanParent}
|
||||
defaultValue={defaults.newPerDay}
|
||||
bind:value={config.newPerDay} />
|
||||
bind:value={$config.newPerDay} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingGraduatingInterval()}
|
||||
subLabel="Days to wait after answering Good on the last learning step."
|
||||
warn={stepsExceedGraduatingInterval || goodExceedsEasy}
|
||||
defaultValue={defaults.graduatingIntervalGood}
|
||||
bind:value={config.graduatingIntervalGood} />
|
||||
bind:value={$config.graduatingIntervalGood} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingEasyInterval()}
|
||||
subLabel="Days to wait after answering Easy on the first learning step."
|
||||
warn={goodExceedsEasy}
|
||||
defaultValue={defaults.graduatingIntervalEasy}
|
||||
bind:value={config.graduatingIntervalEasy} />
|
||||
bind:value={$config.graduatingIntervalEasy} />
|
||||
|
||||
<SpinBoxFloat
|
||||
label={tr.schedulingStartingEase()}
|
||||
@ -77,12 +82,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={1.31}
|
||||
max={5}
|
||||
defaultValue={defaults.initialEase}
|
||||
value={config.initialEase}
|
||||
on:changed={(evt) => (config.initialEase = evt.detail.value)} />
|
||||
value={$config.initialEase}
|
||||
on:changed={(evt) => ($config.initialEase = evt.detail.value)} />
|
||||
|
||||
<CheckBox
|
||||
label="Bury New"
|
||||
subLabel={tr.schedulingBuryRelatedNewCardsUntilThe()}
|
||||
defaultValue={defaults.buryNew}
|
||||
bind:value={config.buryNew} />
|
||||
bind:value={$config.buryNew} />
|
||||
</div>
|
||||
|
@ -2,6 +2,50 @@
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
// import * as tr from "anki/i18n";
|
||||
import { textInputModal } from "./textInputModal";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let state: DeckConfigState;
|
||||
|
||||
function addConfig(): void {
|
||||
textInputModal({
|
||||
title: "Add Config",
|
||||
prompt: "Name:",
|
||||
onOk: (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length) {
|
||||
state.addConfig(trimmed);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renameConfig(): void {
|
||||
textInputModal({
|
||||
title: "Rename Config",
|
||||
prompt: "Name:",
|
||||
startingValue: state.getCurrentName(),
|
||||
onOk: (text: string) => {
|
||||
state.setCurrentName(text);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function removeConfig(): void {
|
||||
setTimeout(() => {
|
||||
if (confirm("Are you sure?")) {
|
||||
try {
|
||||
state.removeCurrentConfig();
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(svg) {
|
||||
vertical-align: text-bottom;
|
||||
@ -18,13 +62,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href={'#'}>Add</a></li>
|
||||
<li><a class="dropdown-item" href={'#'}>Rename</a></li>
|
||||
<li><a class="dropdown-item" href={'#'}>Remove</a></li>
|
||||
<li><a class="dropdown-item" href={'#'} on:click={addConfig}>Add Config</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href={'#'} on:click={renameConfig}>Rename Config</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href={'#'} on:click={removeConfig}>Remove Config</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<input type="checkbox" class="form-check-input" id="dropdownCheck" />
|
||||
<label class="form-check-label" for="dropdownCheck"> Apply to Children </label>
|
||||
<li><a class="dropdown-item" href={'#'}>Apply to Child Decks</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -3,14 +3,15 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type pb from "anki/backend_proto";
|
||||
import * as tr from "anki/i18n";
|
||||
import SpinBox from "./SpinBox.svelte";
|
||||
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||
import CheckBox from "./CheckBox.svelte";
|
||||
import type { DeckConfigState } from "./lib";
|
||||
|
||||
export let config: pb.BackendProto.DeckConfig.Config;
|
||||
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||
export let state: DeckConfigState;
|
||||
let config = state.currentConfig;
|
||||
let defaults = state.defaults;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@ -21,7 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
subLabel="The maximum number of reviews cards to show in a day."
|
||||
min={0}
|
||||
defaultValue={defaults.reviewsPerDay}
|
||||
bind:value={config.reviewsPerDay} />
|
||||
bind:value={$config.reviewsPerDay} />
|
||||
|
||||
<SpinBoxFloat
|
||||
label={tr.schedulingEasyBonus()}
|
||||
@ -29,8 +30,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={1}
|
||||
max={3}
|
||||
defaultValue={defaults.easyMultiplier}
|
||||
value={config.easyMultiplier}
|
||||
on:changed={(evt) => (config.easyMultiplier = evt.detail.value)} />
|
||||
value={$config.easyMultiplier}
|
||||
on:changed={(evt) => ($config.easyMultiplier = evt.detail.value)} />
|
||||
|
||||
<SpinBoxFloat
|
||||
label={tr.schedulingIntervalModifier()}
|
||||
@ -38,8 +39,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={0.5}
|
||||
max={2}
|
||||
defaultValue={defaults.intervalMultiplier}
|
||||
value={config.intervalMultiplier}
|
||||
on:changed={(evt) => (config.intervalMultiplier = evt.detail.value)} />
|
||||
value={$config.intervalMultiplier}
|
||||
on:changed={(evt) => ($config.intervalMultiplier = evt.detail.value)} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingMaximumInterval()}
|
||||
@ -47,7 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={1}
|
||||
max={365 * 100}
|
||||
defaultValue={defaults.maximumReviewInterval}
|
||||
bind:value={config.maximumReviewInterval} />
|
||||
bind:value={$config.maximumReviewInterval} />
|
||||
|
||||
<SpinBoxFloat
|
||||
label={tr.schedulingHardInterval()}
|
||||
@ -55,12 +56,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
min={0.5}
|
||||
max={1.3}
|
||||
defaultValue={defaults.hardMultiplier}
|
||||
value={config.hardMultiplier}
|
||||
on:changed={(evt) => (config.hardMultiplier = evt.detail.value)} />
|
||||
value={$config.hardMultiplier}
|
||||
on:changed={(evt) => ($config.hardMultiplier = evt.detail.value)} />
|
||||
|
||||
<CheckBox
|
||||
label="Bury Reviews"
|
||||
subLabel={tr.schedulingBuryRelatedReviewsUntilTheNext()}
|
||||
defaultValue={defaults.buryReviews}
|
||||
bind:value={config.buryReviews} />
|
||||
bind:value={$config.buryReviews} />
|
||||
</div>
|
||||
|
93
ts/deckconfig/TextInputModal.svelte
Normal file
93
ts/deckconfig/TextInputModal.svelte
Normal file
@ -0,0 +1,93 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
|
||||
export let title: string;
|
||||
export let prompt: string;
|
||||
export let startingValue = "";
|
||||
export let onOk: (text: string) => void;
|
||||
|
||||
let inputRef: HTMLInputElement;
|
||||
let modal: Modal;
|
||||
|
||||
function onShown(): void {
|
||||
inputRef.focus();
|
||||
}
|
||||
|
||||
function onHidden(): void {
|
||||
const container = document.getElementById("modal")!;
|
||||
container.removeChild(container.firstElementChild!);
|
||||
}
|
||||
|
||||
function onOkClicked(): void {
|
||||
onOk(inputRef.value);
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
function onKeyUp(evt: KeyboardEvent): void {
|
||||
if (evt.code === "Enter") {
|
||||
onOkClicked();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const container = document.getElementById("modal")!;
|
||||
container.addEventListener("shown.bs.modal", onShown);
|
||||
container.addEventListener("hidden.bs.modal", onHidden);
|
||||
modal = new Modal(container.firstElementChild!, {});
|
||||
modal.show();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
const container = document.getElementById("modal")!;
|
||||
container.removeEventListener("shown.bs.modal", onShown);
|
||||
container.removeEventListener("hidden.bs.modal", onHidden);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="modal fade" tabindex="-1" aria-labelledby="modalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" on:keyup={onKeyUp}>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalLabel">{title}</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label
|
||||
for="prompt-input"
|
||||
class="col-form-label">{prompt}</label>
|
||||
<input
|
||||
id="prompt-input"
|
||||
bind:this={inputRef}
|
||||
type="text"
|
||||
class="form-control"
|
||||
value={startingValue} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
on:click={onOkClicked}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -5,6 +5,8 @@
|
||||
@import "ts/sass/bootstrap/forms";
|
||||
@import "ts/sass/bootstrap/buttons";
|
||||
@import "ts/sass/bootstrap/button-group";
|
||||
@import "ts/sass/bootstrap/modal";
|
||||
@import "ts/sass/bootstrap/close";
|
||||
|
||||
.night-mode {
|
||||
@include scrollbar.night-mode;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { getDeckConfigInfo, stateFromUpdateData } from "./lib";
|
||||
import { getDeckConfigInfo, DeckConfigState } from "./lib";
|
||||
import { setupI18n, ModuleName } from "anki/i18n";
|
||||
import { checkNightMode } from "anki/nightmode";
|
||||
import DeckConfigPage from "./DeckConfigPage.svelte";
|
||||
@ -15,7 +15,7 @@ export async function deckConfig(
|
||||
modules: [ModuleName.SCHEDULING, ModuleName.ACTIONS, ModuleName.DECK_CONFIG],
|
||||
});
|
||||
const info = await getDeckConfigInfo(deckId);
|
||||
const state = stateFromUpdateData(info);
|
||||
const state = new DeckConfigState(info);
|
||||
new DeckConfigPage({
|
||||
target,
|
||||
props: { state },
|
||||
|
@ -7,6 +7,9 @@
|
||||
|
||||
import pb from "anki/backend_proto";
|
||||
import { postRequest } from "anki/postrequest";
|
||||
import { Writable, writable, get, Readable, readable } from "svelte/store";
|
||||
import { isEqual, cloneDeep } from "lodash-es";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export async function getDeckConfigInfo(
|
||||
deckId: number
|
||||
@ -23,30 +26,148 @@ export interface ConfigWithCount {
|
||||
useCount: number;
|
||||
}
|
||||
|
||||
export interface DeckConfigState {
|
||||
deckName: string;
|
||||
selectedConfigId: DeckConfigId;
|
||||
removedConfigs: DeckConfigId[];
|
||||
renamedConfigs: Map<DeckConfigId, string>;
|
||||
allConfigs: ConfigWithCount[];
|
||||
defaults: pb.BackendProto.DeckConfig.Config;
|
||||
/// Info for showing the top selector
|
||||
export interface ConfigListEntry {
|
||||
idx: number;
|
||||
name: string;
|
||||
useCount: number;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export function stateFromUpdateData(
|
||||
data: pb.BackendProto.DeckConfigForUpdate
|
||||
): DeckConfigState {
|
||||
const current = data.currentDeck as pb.BackendProto.DeckConfigForUpdate.CurrentDeck;
|
||||
return {
|
||||
deckName: current.name,
|
||||
selectedConfigId: current.configId,
|
||||
removedConfigs: [],
|
||||
renamedConfigs: new Map(),
|
||||
allConfigs: data.allConfig.map((config) => {
|
||||
type ConfigInner = pb.BackendProto.DeckConfig.Config;
|
||||
export class DeckConfigState {
|
||||
readonly currentConfig: Writable<ConfigInner>;
|
||||
readonly configList: Readable<ConfigListEntry[]>;
|
||||
readonly currentDeck: pb.BackendProto.DeckConfigForUpdate.CurrentDeck;
|
||||
readonly defaults: ConfigInner;
|
||||
|
||||
private configs: ConfigWithCount[];
|
||||
private selectedIdx: number;
|
||||
private configListSetter?: (val: ConfigListEntry[]) => void;
|
||||
private removedConfigs: DeckConfigId[] = [];
|
||||
|
||||
constructor(data: pb.BackendProto.DeckConfigForUpdate) {
|
||||
this.currentDeck = data.currentDeck as pb.BackendProto.DeckConfigForUpdate.CurrentDeck;
|
||||
this.defaults = data.defaults!.config! as ConfigInner;
|
||||
this.configs = data.allConfig.map((config) => {
|
||||
return {
|
||||
config: config.config as pb.BackendProto.DeckConfig,
|
||||
useCount: config.useCount!,
|
||||
};
|
||||
}),
|
||||
defaults: data.defaults!.config! as pb.BackendProto.DeckConfig.Config,
|
||||
});
|
||||
this.selectedIdx =
|
||||
this.configs.findIndex((c) => c.config.id === this.currentDeck.configId) ??
|
||||
0;
|
||||
// decrement the use count of the starting item, as we'll apply +1 to currently
|
||||
// selected one at display time
|
||||
this.configs[this.selectedIdx].useCount -= 1;
|
||||
this.currentConfig = writable(this.getCurrentConfig());
|
||||
this.configList = readable(this.getConfigList(), (set) => {
|
||||
this.configListSetter = set;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentIndex(index: number): void {
|
||||
this.saveCurrentConfig();
|
||||
this.selectedIdx = index;
|
||||
this.updateCurrentConfig();
|
||||
// use counts have changed
|
||||
this.updateConfigList();
|
||||
}
|
||||
|
||||
/// Persist any changes made to the current config into the list of configs.
|
||||
saveCurrentConfig(): void {
|
||||
const config = get(this.currentConfig);
|
||||
if (!isEqual(config, this.configs[this.selectedIdx].config.config)) {
|
||||
console.log("save");
|
||||
this.configs[this.selectedIdx].config.config = config;
|
||||
this.configs[this.selectedIdx].config.mtimeSecs = 0;
|
||||
} else {
|
||||
console.log("no changes");
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentName(): string {
|
||||
return this.configs[this.selectedIdx].config.name;
|
||||
}
|
||||
|
||||
setCurrentName(name: string): void {
|
||||
if (this.configs[this.selectedIdx].config.name === name) {
|
||||
return;
|
||||
}
|
||||
const uniqueName = this.ensureNewNameUnique(name);
|
||||
this.configs[this.selectedIdx].config.name = uniqueName;
|
||||
this.configs[this.selectedIdx].config.mtimeSecs = 0;
|
||||
this.updateConfigList();
|
||||
}
|
||||
|
||||
/// Adds a new config, making it current.
|
||||
/// not already a new config.
|
||||
addConfig(name: string): void {
|
||||
const uniqueName = this.ensureNewNameUnique(name);
|
||||
const config = pb.BackendProto.DeckConfig.create({
|
||||
id: 0,
|
||||
name: uniqueName,
|
||||
config: cloneDeep(this.defaults),
|
||||
});
|
||||
const configWithCount = { config, useCount: 0 };
|
||||
this.configs.push(configWithCount);
|
||||
this.selectedIdx = this.configs.length - 1;
|
||||
this.updateCurrentConfig();
|
||||
this.updateConfigList();
|
||||
}
|
||||
|
||||
/// Will throw if the default deck is selected.
|
||||
removeCurrentConfig(): void {
|
||||
const currentId = this.configs[this.selectedIdx].config.id;
|
||||
if (currentId === 1) {
|
||||
throw "can't remove default config";
|
||||
}
|
||||
|
||||
if (currentId !== 0) {
|
||||
this.removedConfigs.push(currentId);
|
||||
}
|
||||
this.configs.splice(this.selectedIdx, 1);
|
||||
this.selectedIdx = Math.max(0, this.selectedIdx - 1);
|
||||
this.updateCurrentConfig();
|
||||
this.updateConfigList();
|
||||
}
|
||||
|
||||
private ensureNewNameUnique(name: string): string {
|
||||
if (this.configs.find((e) => e.config.name === name) !== undefined) {
|
||||
return name + (new Date().getTime() / 1000).toFixed(0);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
private updateCurrentConfig(): void {
|
||||
this.currentConfig.set(this.getCurrentConfig());
|
||||
}
|
||||
|
||||
private updateConfigList(): void {
|
||||
this.configListSetter?.(this.getConfigList());
|
||||
}
|
||||
|
||||
/// Returns a copy of the currently selected config.
|
||||
private getCurrentConfig(): ConfigInner {
|
||||
return cloneDeep(this.configs[this.selectedIdx].config.config as ConfigInner);
|
||||
}
|
||||
|
||||
private getConfigList(): ConfigListEntry[] {
|
||||
const list: ConfigListEntry[] = this.configs.map((c, idx) => {
|
||||
const useCount = c.useCount + (idx === this.selectedIdx ? 1 : 0);
|
||||
return {
|
||||
name: c.config.name,
|
||||
current: idx === this.selectedIdx,
|
||||
idx,
|
||||
useCount,
|
||||
};
|
||||
});
|
||||
list.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, tr.i18n.langs, { sensitivity: "base" })
|
||||
);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
23
ts/deckconfig/textInputModal.ts
Normal file
23
ts/deckconfig/textInputModal.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import TextInputModal from "./TextInputModal.svelte";
|
||||
|
||||
export interface TextInputModalProps {
|
||||
title: string;
|
||||
prompt: string;
|
||||
startingValue?: string;
|
||||
onOk: (string) => void;
|
||||
}
|
||||
|
||||
export function textInputModal(props: TextInputModalProps): TextInputModal {
|
||||
const target = document.getElementById("modal")!;
|
||||
return new TextInputModal({
|
||||
target,
|
||||
props,
|
||||
});
|
||||
}
|
@ -100,7 +100,6 @@ jest_test(
|
||||
data = [
|
||||
":test_lib",
|
||||
"//ts:jest.config.js",
|
||||
"//ts:package.json",
|
||||
"@npm//protobufjs",
|
||||
],
|
||||
target_compatible_with = select({
|
||||
|
Loading…
Reference in New Issue
Block a user