Merge pull request #1207 from hgiesel/deckoptionssections2

Deck Options refactoring
This commit is contained in:
Damien Elmes 2021-06-22 09:36:15 +10:00 committed by GitHub
commit 76b005991e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1265 additions and 673 deletions

View file

@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import StickyBar from "components/StickyBar.svelte"; import StickyBar from "components/StickyBar.svelte";
import ButtonToolbar from "components/ButtonToolbar.svelte"; import ButtonToolbar from "components/ButtonToolbar.svelte";
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; import Item from "components/Item.svelte";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import ButtonGroupItem from "components/ButtonGroupItem.svelte"; import ButtonGroupItem from "components/ButtonGroupItem.svelte";
@ -27,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<StickyBar> <StickyBar>
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}> <ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
<ButtonToolbarItem> <Item>
<ButtonGroup class="flex-grow-1"> <ButtonGroup class="flex-grow-1">
<ButtonGroupItem> <ButtonGroupItem>
<SelectButton class="flex-grow-1" on:change={blur}> <SelectButton class="flex-grow-1" on:change={blur}>
@ -42,10 +42,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SelectButton> </SelectButton>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </Item>
<ButtonToolbarItem> <Item>
<SaveButton {state} /> <SaveButton {state} />
</ButtonToolbarItem> </Item>
</ButtonToolbar> </ButtonToolbar>
</StickyBar> </StickyBar>

View file

@ -8,7 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { buttonGroupKey } from "./contextKeys"; import { buttonGroupKey } from "./contextKeys";
import type { Identifier } from "./identifier"; import type { Identifier } from "./identifier";
import { insert, add } from "./identifier"; import { insertElement, appendElement } from "./identifier";
import type { ButtonRegistration } from "./buttons"; import type { ButtonRegistration } from "./buttons";
import { ButtonPosition } from "./buttons"; import { ButtonPosition } from "./buttons";
import type { SvelteComponent } from "./registration"; import type { SvelteComponent } from "./registration";
@ -62,9 +62,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
getDynamicInterface(buttonGroupRef); getDynamicInterface(buttonGroupRef);
const insertButton = (button: SvelteComponent, position: Identifier = 0) => const insertButton = (button: SvelteComponent, position: Identifier = 0) =>
addComponent(button, (added, parent) => insert(added, parent, position)); addComponent(button, (added, parent) =>
insertElement(added, parent, position)
);
const appendButton = (button: SvelteComponent, position: Identifier = -1) => const appendButton = (button: SvelteComponent, position: Identifier = -1) =>
addComponent(button, (added, parent) => add(added, parent, position)); addComponent(button, (added, parent) =>
appendElement(added, parent, position)
);
const showButton = (id: Identifier) => const showButton = (id: Identifier) =>
updateRegistration(({ detach }) => detach.set(false), id); updateRegistration(({ detach }) => detach.set(false), id);

View file

@ -5,12 +5,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript"> <script lang="typescript">
import { setContext } from "svelte"; import { setContext } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import ButtonToolbarItem from "./ButtonToolbarItem.svelte"; import Item from "./Item.svelte";
import type { ButtonGroupRegistration } from "./buttons"; import { sectionKey } from "./contextKeys";
import { buttonToolbarKey } from "./contextKeys";
import type { Identifier } from "./identifier"; import type { Identifier } from "./identifier";
import { insert, add } from "./identifier"; import { insertElement, appendElement } from "./identifier";
import type { SvelteComponent } from "./registration"; import type { SvelteComponent, Registration } from "./registration";
import { makeInterface } from "./registration"; import { makeInterface } from "./registration";
export let id: string | undefined = undefined; export let id: string | undefined = undefined;
@ -30,7 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: style = buttonSize + buttonWrap; $: style = buttonSize + buttonWrap;
function makeRegistration(): ButtonGroupRegistration { function makeRegistration(): Registration {
const detach = writable(false); const detach = writable(false);
return { detach }; return { detach };
} }
@ -38,7 +37,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const { registerComponent, dynamicItems, getDynamicInterface } = const { registerComponent, dynamicItems, getDynamicInterface } =
makeInterface(makeRegistration); makeInterface(makeRegistration);
setContext(buttonToolbarKey, registerComponent); setContext(sectionKey, registerComponent);
export let api: Record<string, unknown> | undefined = undefined; export let api: Record<string, unknown> | undefined = undefined;
let buttonToolbarRef: HTMLDivElement; let buttonToolbarRef: HTMLDivElement;
@ -48,9 +47,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
getDynamicInterface(buttonToolbarRef); getDynamicInterface(buttonToolbarRef);
const insertGroup = (group: SvelteComponent, position: Identifier = 0) => const insertGroup = (group: SvelteComponent, position: Identifier = 0) =>
addComponent(group, (added, parent) => insert(added, parent, position)); addComponent(group, (added, parent) =>
insertElement(added, parent, position)
);
const appendGroup = (group: SvelteComponent, position: Identifier = -1) => const appendGroup = (group: SvelteComponent, position: Identifier = -1) =>
addComponent(group, (added, parent) => add(added, parent, position)); addComponent(group, (added, parent) =>
appendElement(added, parent, position)
);
const showGroup = (id: Identifier) => const showGroup = (id: Identifier) =>
updateRegistration(({ detach }) => detach.set(false), id); updateRegistration(({ detach }) => detach.set(false), id);
@ -75,15 +78,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div <div
bind:this={buttonToolbarRef} bind:this={buttonToolbarRef}
{id} {id}
class={`btn-toolbar wrap-variable ${className}`} class={`btn-toolbar container wrap-variable ${className}`}
{style} {style}
role="toolbar" role="toolbar"
> >
<slot /> <slot />
{#each $dynamicItems as item} {#each $dynamicItems as item}
<ButtonToolbarItem id={item[0].id} registration={item[1]}> <Item id={item[0].id} registration={item[1]}>
<svelte:component this={item[0].component} {...item[0].props} /> <svelte:component this={item[0].component} {...item[0].props} />
</ButtonToolbarItem> </Item>
{/each} {/each}
</div> </div>

View file

@ -0,0 +1,19 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Section from "./Section.svelte";
export let id: string | undefined = undefined;
let className: string = "";
export { className as class };
export let api: Record<string, never> | undefined = undefined;
</script>
<div {id} class={`container mb-1 ${className}`}>
<Section {api}>
<slot />
</Section>
</div>

View file

@ -5,23 +5,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript"> <script lang="typescript">
import Detachable from "components/Detachable.svelte"; import Detachable from "components/Detachable.svelte";
import type { ButtonGroupRegistration } from "./buttons"; import type { Register, Registration } from "./registration";
import type { Register } from "./registration";
import { getContext, hasContext } from "svelte"; import { getContext, hasContext } from "svelte";
import { buttonToolbarKey } from "./contextKeys"; import { sectionKey } from "./contextKeys";
export let id: string | undefined = undefined; export let id: string | undefined = undefined;
export let registration: ButtonGroupRegistration | undefined = undefined; export let registration: Registration | undefined = undefined;
let detached: boolean; let detached: boolean;
if (registration) { if (registration) {
const { detach } = registration; const { detach } = registration;
detach.subscribe((value: boolean) => (detached = value)); detach.subscribe((value: boolean) => (detached = value));
} else if (hasContext(buttonToolbarKey)) { } else if (hasContext(sectionKey)) {
const registerComponent = const registerComponent = getContext<Register<Registration>>(sectionKey);
getContext<Register<ButtonGroupRegistration>>(buttonToolbarKey);
const { detach } = registerComponent(); const { detach } = registerComponent();
detach.subscribe((value: boolean) => (detached = value)); detach.subscribe((value: boolean) => (detached = value));
} else { } else {

View file

@ -0,0 +1,69 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import { setContext } from "svelte";
import { writable } from "svelte/store";
import Item from "./Item.svelte";
import { sectionKey } from "./contextKeys";
import type { Identifier } from "./identifier";
import { insertElement, appendElement } from "./identifier";
import type { SvelteComponent, Registration } from "./registration";
import { makeInterface } from "./registration";
export let id: string | undefined = undefined;
function makeRegistration(): Registration {
const detach = writable(false);
return { detach };
}
const { registerComponent, dynamicItems, getDynamicInterface } =
makeInterface(makeRegistration);
setContext(sectionKey, registerComponent);
export let api: Record<string, never> | undefined = undefined;
let sectionRef: HTMLDivElement;
$: if (sectionRef && api) {
const { addComponent, updateRegistration } = getDynamicInterface(sectionRef);
const insert = (group: SvelteComponent, position: Identifier = 0) =>
addComponent(group, (added, parent) =>
insertElement(added, parent, position)
);
const append = (group: SvelteComponent, position: Identifier = -1) =>
addComponent(group, (added, parent) =>
appendElement(added, parent, position)
);
const show = (id: Identifier) =>
updateRegistration(({ detach }) => detach.set(false), id);
const hide = (id: Identifier) =>
updateRegistration(({ detach }) => detach.set(true), id);
const toggle = (id: Identifier) =>
updateRegistration(
({ detach }) => detach.update((old: boolean): boolean => !old),
id
);
Object.assign(api, { insert, append, show, hide, toggle });
}
</script>
<div bind:this={sectionRef} {id}>
<slot />
{#each $dynamicItems as item}
<Item id={item[0].id} registration={item[1]}>
<svelte:component this={item[0].component} {...item[0].props} />
</Item>
{/each}
</div>
<style lang="scss">
div {
display: contents;
}
</style>

View file

@ -35,6 +35,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
class="{className} form-select" class="{className} form-select"
class:btn-day={!nightMode} class:btn-day={!nightMode}
class:btn-night={nightMode} class:btn-night={nightMode}
class:visible-down-arrow={nightMode}
title={tooltip} title={tooltip}
on:change on:change
> >
@ -50,6 +51,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
overflow-x: hidden; overflow-x: hidden;
} }
.visible-down-arrow {
/* override the default down arrow */
background-image: button.down-arrow(white);
}
@include button.btn-day($with-hover: false); @include button.btn-day($with-hover: false);
@include button.btn-night($with-hover: false); @include button.btn-night($with-hover: false);
</style> </style>

View file

@ -4,11 +4,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="typescript"> <script lang="typescript">
export let id: string | undefined = undefined; export let id: string | undefined = undefined;
let className: string | undefined; let className: string = "";
export { className as class }; export { className as class };
</script> </script>
<nav {id} class={`pb-1 pt-1 ${className}`}> <nav {id} class={`container-fluid pb-1 pt-1 ${className}`}>
<slot /> <slot />
</nav> </nav>

View file

@ -8,6 +8,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { setContext } from "svelte"; import { setContext } from "svelte";
import { dropdownKey } from "./contextKeys"; import { dropdownKey } from "./contextKeys";
export let disabled = false;
setContext(dropdownKey, { setContext(dropdownKey, {
dropdown: true, dropdown: true,
"data-bs-toggle": "dropdown", "data-bs-toggle": "dropdown",
@ -17,26 +19,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const menuId = Math.random().toString(36).substring(2); const menuId = Math.random().toString(36).substring(2);
let dropdown: Dropdown; let dropdown: Dropdown;
function activateDropdown(_event: MouseEvent): void { function activateDropdown(): void {
dropdown.toggle(); if (!disabled) {
dropdown.toggle();
}
} }
/* Normally dropdown and trigger are associated with a /* Normally dropdown and trigger are associated with a
/* common ancestor with .dropdown class */ /* common ancestor with .dropdown class */
function createDropdown(event: CustomEvent): void { function createDropdown(element: HTMLElement): void {
const button: HTMLButtonElement = event.detail.button;
/* Prevent focus on menu activation */ /* Prevent focus on menu activation */
const noop = () => {}; const noop = () => {};
Object.defineProperty(button, "focus", { value: noop }); Object.defineProperty(element, "focus", { value: noop });
const menu = (button.getRootNode() as Document) /* or shadow root */ const menu = (element.getRootNode() as Document) /* or shadow root */
.getElementById(menuId); .getElementById(menuId);
if (!menu) { if (!menu) {
console.log(`Could not find menu "${menuId}" for dropdown menu.`); console.log(`Could not find menu "${menuId}" for dropdown menu.`);
} else { } else {
dropdown = new Dropdown(button); dropdown = new Dropdown(element);
/* Set custom menu without using common element with .dropdown */ /* Set custom menu without using common element with .dropdown */
(dropdown as any)._menu = menu; (dropdown as any)._menu = menu;

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { Registration } from "./registration";
export enum ButtonPosition { export enum ButtonPosition {
Standalone, Standalone,
@ -9,11 +10,6 @@ export enum ButtonPosition {
Rightmost, Rightmost,
} }
export interface ButtonRegistration { export interface ButtonRegistration extends Registration {
detach: Writable<boolean>;
position: Writable<ButtonPosition>; position: Writable<ButtonPosition>;
} }
export interface ButtonGroupRegistration {
detach: Writable<boolean>;
}

View file

@ -1,9 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export const nightModeKey = Symbol("nightMode"); export const nightModeKey = Symbol("nightMode");
export const touchDeviceKey = Symbol("touchDevice");
export const disabledKey = Symbol("disabled"); export const disabledKey = Symbol("disabled");
export const buttonToolbarKey = Symbol("buttonToolbar"); export const sectionKey = Symbol("section");
export const buttonGroupKey = Symbol("buttonGroup"); export const buttonGroupKey = Symbol("buttonGroup");
export const dropdownKey = Symbol("dropdown"); export const dropdownKey = Symbol("dropdown");
export const modalsKey = Symbol("modals"); export const modalsKey = Symbol("modals");

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export type Identifier = string | number; export type Identifier = string | number;
export function find( export function findElement(
collection: HTMLCollection, collection: HTMLCollection,
idOrIndex: Identifier idOrIndex: Identifier
): [number, Element] | null { ): [number, Element] | null {
@ -34,12 +34,12 @@ export function find(
return result; return result;
} }
export function insert( export function insertElement(
element: Element, element: Element,
collection: Element, collection: Element,
idOrIndex: Identifier idOrIndex: Identifier
): number { ): number {
const match = find(collection.children, idOrIndex); const match = findElement(collection.children, idOrIndex);
if (match) { if (match) {
const [index, reference] = match; const [index, reference] = match;
@ -51,12 +51,12 @@ export function insert(
return -1; return -1;
} }
export function add( export function appendElement(
element: Element, element: Element,
collection: Element, collection: Element,
idOrIndex: Identifier idOrIndex: Identifier
): number { ): number {
const match = find(collection.children, idOrIndex); const match = findElement(collection.children, idOrIndex);
if (match) { if (match) {
const [index, before] = match; const [index, before] = match;
@ -69,12 +69,12 @@ export function add(
return -1; return -1;
} }
export function update( export function updateElement(
f: (element: Element) => void, f: (element: Element) => void,
collection: Element, collection: Element,
idOrIndex: Identifier idOrIndex: Identifier
): number { ): number {
const match = find(collection.children, idOrIndex); const match = findElement(collection.children, idOrIndex);
if (match) { if (match) {
const [index, element] = match; const [index, element] = match;

View file

@ -1,10 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { SvelteComponentTyped } from "svelte/internal"; import type { SvelteComponentTyped } from "svelte/internal";
import type { Readable } from "svelte/store"; import type { Writable, Readable } from "svelte/store";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { Identifier } from "./identifier"; import type { Identifier } from "./identifier";
import { find } from "./identifier"; import { findElement } from "./identifier";
export interface SvelteComponent { export interface SvelteComponent {
component: SvelteComponentTyped; component: SvelteComponentTyped;
@ -12,9 +12,13 @@ export interface SvelteComponent {
props: Record<string, unknown> | undefined; props: Record<string, unknown> | undefined;
} }
export type Register<T> = (index?: number, registration?: T) => T; export interface Registration {
detach: Writable<boolean>;
}
export interface RegistrationAPI<T> { export type Register<T extends Registration> = (index?: number, registration?: T) => T;
export interface RegistrationAPI<T extends Registration> {
registerComponent: Register<T>; registerComponent: Register<T>;
items: Readable<T[]>; items: Readable<T[]>;
dynamicItems: Readable<[SvelteComponent, T][]>; dynamicItems: Readable<[SvelteComponent, T][]>;
@ -36,7 +40,9 @@ export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE; return node.nodeType === Node.ELEMENT_NODE;
} }
export function makeInterface<T>(makeRegistration: () => T): RegistrationAPI<T> { export function makeInterface<T extends Registration>(
makeRegistration: () => T
): RegistrationAPI<T> {
const registrations: T[] = []; const registrations: T[] = [];
const items = writable(registrations); const items = writable(registrations);
@ -92,7 +98,7 @@ export function makeInterface<T>(makeRegistration: () => T): RegistrationAPI<T>
update: (registration: T) => void, update: (registration: T) => void,
position: Identifier position: Identifier
): void { ): void {
const match = find(elementRef.children, position); const match = findElement(elementRef.children, position);
if (match) { if (match) {
const [index] = match; const [index] = match;

View file

@ -3,18 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
// import * as tr from "lib/i18n"; import TitledContainer from "./TitledContainer.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api: Record<string, never>;
let components = state.addonComponents; let components = state.addonComponents;
const auxData = state.currentAuxData; const auxData = state.currentAuxData;
</script> </script>
{#if $components.length || state.haveAddons} {#if $components.length || state.haveAddons}
<div> <TitledContainer title="Add-ons" {api}>
<h2>Add-ons</h2>
<p> <p>
If you're using an add-on that hasn't been updated to use this new screen If you're using an add-on that hasn't been updated to use this new screen
yet, you can access the old deck options screen by holding down the shift yet, you can access the old deck options screen by holding down the shift
@ -24,5 +24,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{#each $components as addon} {#each $components as addon}
<svelte:component this={addon.component} bind:data={$auxData} {...addon} /> <svelte:component this={addon.component} bind:data={$auxData} {...addon} />
{/each} {/each}
</div> </TitledContainer>
{/if} {/if}

View file

@ -4,72 +4,88 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte"; import TitledContainer from "./TitledContainer.svelte";
import SpinBoxFloat from "./SpinBoxFloat.svelte"; import Item from "components/Item.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte";
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
</script> </script>
<h2>{tr.deckConfigAdvancedTitle()}</h2> <TitledContainer title={tr.deckConfigAdvancedTitle()} {api}>
<Item>
<SpinBoxRow
bind:value={$config.maximumReviewInterval}
defaultValue={defaults.maximumReviewInterval}
min={1}
max={365 * 100}
markdownTooltip={tr.deckConfigMaximumIntervalTooltip()}
>
{tr.schedulingMaximumInterval()}
</SpinBoxRow>
</Item>
<SpinBox <Item>
label={tr.schedulingMaximumInterval()} <SpinBoxFloatRow
tooltip={tr.deckConfigMaximumIntervalTooltip()} bind:value={$config.initialEase}
min={1} defaultValue={defaults.initialEase}
max={365 * 100} min={1.31}
defaultValue={defaults.maximumReviewInterval} max={5}
bind:value={$config.maximumReviewInterval} markdownTooltip={tr.deckConfigStartingEaseTooltip()}
/> >
{tr.schedulingStartingEase()}
</SpinBoxFloatRow>
</Item>
<SpinBoxFloat <Item>
label={tr.schedulingStartingEase()} <SpinBoxFloatRow
tooltip={tr.deckConfigStartingEaseTooltip()} bind:value={$config.easyMultiplier}
min={1.31} defaultValue={defaults.easyMultiplier}
max={5} min={1}
defaultValue={defaults.initialEase} max={3}
value={$config.initialEase} markdownTooltip={tr.deckConfigEasyBonusTooltip()}
on:changed={(evt) => ($config.initialEase = evt.detail.value)} >
/> {tr.schedulingEasyBonus()}
</SpinBoxFloatRow>
</Item>
<SpinBoxFloat <Item>
label={tr.schedulingEasyBonus()} <SpinBoxFloatRow
tooltip={tr.deckConfigEasyBonusTooltip()} bind:value={$config.intervalMultiplier}
min={1} defaultValue={defaults.intervalMultiplier}
max={3} min={0.5}
defaultValue={defaults.easyMultiplier} max={2}
value={$config.easyMultiplier} markdownTooltip={tr.deckConfigIntervalModifierTooltip()}
on:changed={(evt) => ($config.easyMultiplier = evt.detail.value)} >
/> {tr.schedulingIntervalModifier()}
</SpinBoxFloatRow>
</Item>
<SpinBoxFloat <Item>
label={tr.schedulingIntervalModifier()} <SpinBoxFloatRow
tooltip={tr.deckConfigIntervalModifierTooltip()} bind:value={$config.hardMultiplier}
min={0.5} defaultValue={defaults.hardMultiplier}
max={2} min={0.5}
defaultValue={defaults.intervalMultiplier} max={1.3}
value={$config.intervalMultiplier} markdownTooltip={tr.deckConfigHardIntervalTooltip()}
on:changed={(evt) => ($config.intervalMultiplier = evt.detail.value)} >
/> {tr.schedulingHardInterval()}
</SpinBoxFloatRow>
</Item>
<SpinBoxFloat <Item>
label={tr.schedulingHardInterval()} <SpinBoxFloatRow
tooltip={tr.deckConfigHardIntervalTooltip()} bind:value={$config.lapseMultiplier}
min={0.5} defaultValue={defaults.lapseMultiplier}
max={1.3} max={1}
defaultValue={defaults.hardMultiplier} markdownTooltip={tr.deckConfigNewIntervalTooltip()}
value={$config.hardMultiplier} >
on:changed={(evt) => ($config.hardMultiplier = evt.detail.value)} {tr.schedulingNewInterval()}
/> </SpinBoxFloatRow>
</Item>
<SpinBoxFloat </TitledContainer>
label={tr.schedulingNewInterval()}
tooltip={tr.deckConfigNewIntervalTooltip()}
min={0}
max={1}
defaultValue={defaults.lapseMultiplier}
value={$config.lapseMultiplier}
on:changed={(evt) => ($config.lapseMultiplier = evt.detail.value)}
/>

View file

@ -0,0 +1,38 @@
<!--
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 "lib/i18n";
import TitledContainer from "./TitledContainer.svelte";
import Item from "components/Item.svelte";
import SwitchRow from "./SwitchRow.svelte";
import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig;
let defaults = state.defaults;
</script>
<TitledContainer title={tr.deckConfigAudioTitle()} {api}>
<Item>
<SwitchRow
bind:value={$config.disableAutoplay}
defaultValue={defaults.disableAutoplay}
>
{tr.deckConfigDisableAutoplay()}
</SwitchRow>
</Item>
<Item>
<SwitchRow
bind:value={$config.skipQuestionWhenReplayingAnswer}
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
markdownTooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
>
{tr.schedulingAlwaysIncludeQuestionSideWhenReplaying()}
</SwitchRow>
</Item>
</TitledContainer>

View file

@ -38,6 +38,7 @@ copy_bootstrap_icons(
icons = [ icons = [
"arrow-counterclockwise.svg", "arrow-counterclockwise.svg",
"info-circle.svg", "info-circle.svg",
"gear.svg",
], ],
) )
@ -133,6 +134,7 @@ svelte_check(
"*.svelte", "*.svelte",
]) + [ ]) + [
"//ts/sass:button_mixins_lib", "//ts/sass:button_mixins_lib",
"//ts/sass:night_mode_lib",
"//ts/sass/bootstrap", "//ts/sass/bootstrap",
"@npm//@types/bootstrap", "@npm//@types/bootstrap",
"@npm//@types/lodash-es", "@npm//@types/lodash-es",

View file

@ -0,0 +1,46 @@
<!--
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 { DropdownProps } from "components/dropdown";
import { dropdownKey } from "components/contextKeys";
import { onMount, createEventDispatcher, getContext } from "svelte";
let className = "";
export { className as class };
const dispatch = createEventDispatcher();
let spanRef: HTMLSpanElement;
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
onMount(() => {
dispatch("mount", { span: spanRef });
});
</script>
<span
bind:this={spanRef}
class={`badge ${className}`}
class:dropdown-toggle={dropdownProps.dropdown}
{...dropdownProps}
on:click
>
<slot />
</span>
<style>
.badge {
color: inherit;
}
.dropdown-toggle::after {
display: none;
}
span :global(svg) {
vertical-align: -0.125rem;
}
</style>

View file

@ -4,26 +4,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import CheckBox from "./CheckBox.svelte"; import TitledContainer from "./TitledContainer.svelte";
import Item from "components/Item.svelte";
import SwitchRow from "./SwitchRow.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
</script> </script>
<h2>{tr.deckConfigBuryTitle()}</h2> <TitledContainer title={tr.deckConfigBuryTitle()} {api}>
<Item>
<SwitchRow
bind:value={$config.buryNew}
defaultValue={defaults.buryNew}
markdownTooltip={tr.deckConfigBuryTooltip()}
>
{tr.deckConfigBuryNewSiblings()}
</SwitchRow>
</Item>
<CheckBox <Item>
label={tr.deckConfigBuryNewSiblings()} <SwitchRow
tooltip={tr.deckConfigBuryTooltip()} bind:value={$config.buryReviews}
defaultValue={defaults.buryNew} defaultValue={defaults.buryReviews}
bind:value={$config.buryNew} markdownTooltip={tr.deckConfigBuryTooltip()}
/> >
{tr.deckConfigBuryReviewSiblings()}
<CheckBox </SwitchRow>
label={tr.deckConfigBuryReviewSiblings()} </Item>
tooltip={tr.deckConfigBuryTooltip()} </TitledContainer>
defaultValue={defaults.buryReviews}
bind:value={$config.buryReviews}
/>

View file

@ -3,26 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import ConfigEntry from "./ConfigEntry.svelte";
import HelpPopup from "./HelpPopup.svelte";
export let label: string;
export let value: boolean; export let value: boolean;
export let defaultValue: boolean;
export let tooltip = "";
export let id: string | undefined = undefined;
</script> </script>
<ConfigEntry {id} label="" wholeLine={true} bind:value {defaultValue}> <label> <input type="checkbox" bind:checked={value} /> <slot /> </label>
<div class="checkbox-outer">
<label> <input type="checkbox" bind:checked={value} /> {label} </label>
{#if tooltip}
<HelpPopup html={tooltip} />
{/if}
</div>
</ConfigEntry>
<style lang="scss">
.checkbox-outer {
margin-top: 0.5em;
}
</style>

View file

@ -0,0 +1,26 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import Label from "./Label.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import CheckBox from "./CheckBox.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: boolean;
export let defaultValue: boolean;
export let markdownTooltip: string | undefined = undefined;
</script>
<Row>
<Col>
<RevertButton bind:value {defaultValue} />
<CheckBox bind:value
>{#if markdownTooltip}<TooltipLabel {markdownTooltip}><slot /></TooltipLabel
>{:else}<Label><slot /></Label>{/if}</CheckBox
>
</Col>
</Row>

22
ts/deckoptions/Col.svelte Normal file
View file

@ -0,0 +1,22 @@
<!--
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 { Breakpoint, Size } from "./col";
export let breakpoint: Breakpoint | undefined = undefined;
export let size: Size | undefined = undefined;
export let grow = true;
let colClass: string;
$: {
const breakpointComponent = breakpoint ? `-${breakpoint}` : "";
const sizeComponent = size ? `-${size}` : "";
colClass = "col" + breakpointComponent + sizeComponent;
}
</script>
<div class={`${colClass} d-flex align-items-center`} class:flex-grow-0={!grow}>
<slot />
</div>

View file

@ -1,48 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import DailyLimits from "./DailyLimits.svelte";
import DisplayOrder from "./DisplayOrder.svelte";
import NewOptions from "./NewOptions.svelte";
import AdvancedOptions from "./AdvancedOptions.svelte";
import BuryOptions from "./BuryOptions.svelte";
import LapseOptions from "./LapseOptions.svelte";
import GeneralOptions from "./GeneralOptions.svelte";
import Addons from "./Addons.svelte";
import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState;
</script>
<div class="outer">
<DailyLimits {state} />
<NewOptions {state} />
<LapseOptions {state} />
<BuryOptions {state} />
{#if state.v3Scheduler}
<DisplayOrder {state} />
{/if}
<GeneralOptions {state} />
<Addons {state} />
<AdvancedOptions {state} />
</div>
<style lang="scss">
:global(h2) {
margin-top: 1em;
font-weight: bold;
// adding a border decreases the default font size,
// so increase it again
font-size: 2em;
border-bottom: 1px solid var(--medium-border);
margin-right: 16px;
margin-bottom: 0.5em;
}
.outer {
// the right margin has an indent to allow for the undo
// buttons; add the same indent on the left for balance
padding-left: 16px;
}
</style>

View file

@ -1,80 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import marked from "marked";
import { slide } from "svelte/transition";
import RevertButton from "./RevertButton.svelte";
import HelpPopup from "./HelpPopup.svelte";
export let label: string;
export let tooltip = "";
export let value: any;
export let defaultValue: any;
/// empty strings will be ignored
export let warnings: string[] = [];
export let wholeLine = false;
export let id: string | undefined = undefined;
let renderedTooltip: string;
$: renderedTooltip = marked(tooltip);
</script>
<div {id} class="outer">
{#if label}
<div class="table">
<span class="vcenter">
{label}
{#if renderedTooltip}
<HelpPopup html={renderedTooltip} />
{/if}
</span>
</div>
{/if}
<div class="input-grid" class:full-grid-width={wholeLine}>
<slot />
<RevertButton bind:value {defaultValue} on:revert />
</div>
<div class="full-grid-width">
{#each warnings as warning}
{#if warning}
<div class="alert alert-warning" in:slide out:slide>{warning}</div>
{/if}
{/each}
</div>
</div>
<style lang="scss">
.outer {
display: grid;
grid-template-columns: 7fr 3fr;
grid-row-gap: 0.5em;
}
.full-grid-width {
grid-column: 1 / 6;
}
.table {
display: table;
height: 100%;
}
.vcenter {
display: table-cell;
vertical-align: middle;
}
.alert {
margin-top: 0.5em;
}
.input-grid {
display: grid;
grid-column-gap: 0.5em;
grid-template-columns: 10fr 16px;
}
</style>

View file

@ -12,7 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import TextInputModal from "./TextInputModal.svelte"; import TextInputModal from "./TextInputModal.svelte";
import StickyBar from "components/StickyBar.svelte"; import StickyBar from "components/StickyBar.svelte";
import ButtonToolbar from "components/ButtonToolbar.svelte"; import ButtonToolbar from "components/ButtonToolbar.svelte";
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; import Item from "components/Item.svelte";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import ButtonGroupItem from "components/ButtonGroupItem.svelte"; import ButtonGroupItem from "components/ButtonGroupItem.svelte";
@ -87,9 +87,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bind:modalKey bind:modalKey
/> />
<StickyBar> <StickyBar class="g-1">
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}> <ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
<ButtonToolbarItem> <Item>
<ButtonGroup class="flex-grow-1"> <ButtonGroup class="flex-grow-1">
<ButtonGroupItem> <ButtonGroupItem>
<SelectButton class="flex-grow-1" on:change={blur}> <SelectButton class="flex-grow-1" on:change={blur}>
@ -104,15 +104,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SelectButton> </SelectButton>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </Item>
<ButtonToolbarItem> <Item>
<SaveButton <SaveButton
{state} {state}
on:add={promptToAdd} on:add={promptToAdd}
on:clone={promptToClone} on:clone={promptToClone}
on:rename={promptToRename} on:rename={promptToRename}
/> />
</ButtonToolbarItem> </Item>
</ButtonToolbar> </ButtonToolbar>
</StickyBar> </StickyBar>

View file

@ -1,13 +1,18 @@
<!-- <!--
Copyright: Ankitects Pty Ltd and contributors Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte"; import TitledContainer from "./TitledContainer.svelte";
import Item from "components/Item.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte";
import Warning from "./Warning.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
let parentLimits = state.parentLimits; let parentLimits = state.parentLimits;
@ -33,22 +38,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: ""; : "";
</script> </script>
<h2>{tr.deckConfigDailyLimits()}</h2> <TitledContainer title={tr.deckConfigDailyLimits()} {api}>
<Item>
<SpinBoxRow
bind:value={$config.newPerDay}
defaultValue={defaults.newPerDay}
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
>
{tr.schedulingNewCardsday()}
</SpinBoxRow>
<SpinBox <Warning warning={newCardsGreaterThanParent} />
label={tr.schedulingNewCardsday()} </Item>
tooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
min={0}
warnings={[newCardsGreaterThanParent]}
defaultValue={defaults.newPerDay}
bind:value={$config.newPerDay}
/>
<SpinBox <Item>
label={tr.schedulingMaximumReviewsday()} <SpinBoxRow
tooltip={tr.deckConfigReviewLimitTooltip() + v3Extra} bind:value={$config.reviewsPerDay}
min={0} defaultValue={defaults.reviewsPerDay}
warnings={[reviewsTooLow]} markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
defaultValue={defaults.reviewsPerDay} >
bind:value={$config.reviewsPerDay} {tr.schedulingMaximumReviewsday()}
/> </SpinBoxRow>
<Warning warning={reviewsTooLow} />
</Item>
</TitledContainer>

View file

@ -4,7 +4,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import ConfigSelector from "./ConfigSelector.svelte"; import ConfigSelector from "./ConfigSelector.svelte";
import ConfigEditor from "./ConfigEditor.svelte"; import Container from "components/Container.svelte";
import Item from "components/Item.svelte";
import DailyLimits from "./DailyLimits.svelte";
import DisplayOrder from "./DisplayOrder.svelte";
import NewOptions from "./NewOptions.svelte";
import AdvancedOptions from "./AdvancedOptions.svelte";
import BuryOptions from "./BuryOptions.svelte";
import LapseOptions from "./LapseOptions.svelte";
import TimerOptions from "./TimerOptions.svelte";
import AudioOptions from "./AudioOptions.svelte";
import Addons from "./Addons.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import HtmlAddon from "./HtmlAddon.svelte"; import HtmlAddon from "./HtmlAddon.svelte";
@ -31,7 +42,52 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}, },
]; ];
} }
export const options = {};
export const dailyLimits = {};
export const newOptions = {};
export const lapseOptions = {};
export const buryOptions = {};
export const displayOrder = {};
export const timerOptions = {};
export const audioOptions = {};
export const addonOptions = {};
export const advancedOptions = {};
</script> </script>
<ConfigSelector {state} /> <ConfigSelector {state} />
<ConfigEditor {state} />
<Container api={options} class="g-1">
<Item>
<DailyLimits {state} api={dailyLimits} />
</Item>
<Item>
<NewOptions {state} api={newOptions} />
</Item>
<Item>
<LapseOptions {state} api={lapseOptions} />
</Item>
<Item>
<BuryOptions {state} api={buryOptions} />
</Item>
{#if state.v3Scheduler}
<Item>
<DisplayOrder {state} api={displayOrder} />
</Item>
{/if}
<Item>
<TimerOptions {state} api={timerOptions} />
</Item>
<Item>
<AudioOptions {state} api={audioOptions} />
</Item>
<Item>
<Addons {state} api={addonOptions} />
</Item>
<Item>
<AdvancedOptions {state} api={advancedOptions} />
</Item>
</Container>

View file

@ -4,12 +4,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import EnumSelector from "./EnumSelector.svelte"; import TitledContainer from "./TitledContainer.svelte";
import Item from "components/Item.svelte";
import EnumSelectorRow from "./EnumSelectorRow.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
import { reviewMixChoices } from "./strings"; import { reviewMixChoices } from "./strings";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
@ -35,44 +39,59 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
]; ];
</script> </script>
<h2>{tr.deckConfigOrderingTitle()}</h2> <TitledContainer title={tr.deckConfigOrderingTitle()} {api}>
<Item>
<EnumSelectorRow
bind:value={$config.newCardGatherPriority}
defaultValue={defaults.newCardGatherPriority}
choices={newGatherPriorityChoices}
markdownTooltip={tr.deckConfigNewGatherPriorityTooltip()}
>
{tr.deckConfigNewGatherPriority()}
</EnumSelectorRow>
</Item>
<EnumSelector <Item>
label={tr.deckConfigNewGatherPriority()} <EnumSelectorRow
tooltip={tr.deckConfigNewGatherPriorityTooltip()} bind:value={$config.newCardSortOrder}
choices={newGatherPriorityChoices} defaultValue={defaults.newCardSortOrder}
defaultValue={defaults.newCardGatherPriority} choices={newSortOrderChoices}
bind:value={$config.newCardGatherPriority} markdownTooltip={tr.deckConfigNewCardSortOrderTooltip()}
/> >
{tr.deckConfigNewCardSortOrder()}
</EnumSelectorRow>
</Item>
<EnumSelector <Item>
label={tr.deckConfigNewCardSortOrder()} <EnumSelectorRow
tooltip={tr.deckConfigNewCardSortOrderTooltip()} bind:value={$config.newMix}
choices={newSortOrderChoices} defaultValue={defaults.newMix}
defaultValue={defaults.newCardSortOrder} choices={reviewMixChoices()}
bind:value={$config.newCardSortOrder} markdownTooltip={tr.deckConfigNewReviewPriorityTooltip()}
/> >
{tr.deckConfigNewReviewPriority()}
</EnumSelectorRow>
</Item>
<EnumSelector <Item>
label={tr.deckConfigNewReviewPriority()} <EnumSelectorRow
tooltip={tr.deckConfigNewReviewPriorityTooltip()} bind:value={$config.interdayLearningMix}
choices={reviewMixChoices()} defaultValue={defaults.interdayLearningMix}
defaultValue={defaults.newMix} choices={reviewMixChoices()}
bind:value={$config.newMix} markdownTooltip={tr.deckConfigInterdayStepPriorityTooltip()}
/> >
{tr.deckConfigInterdayStepPriority()}
</EnumSelectorRow>
</Item>
<EnumSelector <Item>
label={tr.deckConfigInterdayStepPriority()} <EnumSelectorRow
tooltip={tr.deckConfigInterdayStepPriorityTooltip()} bind:value={$config.reviewOrder}
choices={reviewMixChoices()} defaultValue={defaults.reviewOrder}
defaultValue={defaults.interdayLearningMix} choices={reviewOrderChoices}
bind:value={$config.interdayLearningMix} markdownTooltip={tr.deckConfigReviewSortOrderTooltip()}
/> >
{tr.deckConfigReviewSortOrder()}
<EnumSelector </EnumSelectorRow>
label={tr.deckConfigReviewSortOrder()} </Item>
tooltip={tr.deckConfigReviewSortOrderTooltip()} </TitledContainer>
choices={reviewOrderChoices}
defaultValue={defaults.reviewOrder}
bind:value={$config.reviewOrder}
/>

View file

@ -3,19 +3,36 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import ConfigEntry from "./ConfigEntry.svelte"; import { getContext } from "svelte";
import { nightModeKey } from "components/contextKeys";
export let label: string;
export let choices: string[]; export let choices: string[];
export let value: number = 0; export let value: number = 0;
export let defaultValue: number;
export let tooltip = ""; const nightMode = getContext<boolean>(nightModeKey);
</script> </script>
<ConfigEntry {label} {tooltip} wholeLine={true} bind:value {defaultValue}> <select
<select bind:value class="form-select"> bind:value
{#each choices as choice, idx} class:nightMode
<option value={idx}>{choice}</option> class:visible-down-arrow={nightMode}
{/each} class="form-select"
</select> >
</ConfigEntry> {#each choices as choice, idx}
<option value={idx}>{choice}</option>
{/each}
</select>
<style lang="scss">
@use "ts/sass/night_mode" as nightmode;
@use "ts/sass/button_mixins" as button;
.nightMode {
@include nightmode.input;
}
.visible-down-arrow {
/* override the default down arrow */
background-image: button.down-arrow(white);
}
</style>

View file

@ -0,0 +1,29 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { Breakpoint } from "./col";
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import EnumSelector from "./EnumSelector.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: number;
export let defaultValue: number;
export let breakpoint: Breakpoint = "md";
export let choices: string[];
export let markdownTooltip: string;
</script>
<Row>
<Col size={7}>
<RevertButton bind:value {defaultValue} />
<TooltipLabel {markdownTooltip}><slot /></TooltipLabel>
</Col>
<Col {breakpoint} size={5}>
<EnumSelector bind:value {choices} />
</Col>
</Row>

View file

@ -1,48 +0,0 @@
<!--
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 "lib/i18n";
import SpinBox from "./SpinBox.svelte";
import CheckBox from "./CheckBox.svelte";
import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState;
let config = state.currentConfig;
let defaults = state.defaults;
</script>
<h2>{tr.deckConfigTimerTitle()}</h2>
<SpinBox
label={tr.deckConfigMaximumAnswerSecs()}
tooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
min={30}
max={600}
defaultValue={defaults.capAnswerTimeToSecs}
bind:value={$config.capAnswerTimeToSecs}
/>
<CheckBox
id="showAnswerTimer"
label={tr.schedulingShowAnswerTimer()}
tooltip={tr.deckConfigShowAnswerTimerTooltip()}
defaultValue={defaults.showTimer}
bind:value={$config.showTimer}
/>
<h2>{tr.deckConfigAudioTitle()}</h2>
<CheckBox
label={tr.deckConfigDisableAutoplay()}
defaultValue={defaults.disableAutoplay}
bind:value={$config.disableAutoplay}
/>
<CheckBox
label={tr.schedulingAlwaysIncludeQuestionSideWhenReplaying()}
tooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
bind:value={$config.skipQuestionWhenReplayingAnswer}
/>

View file

@ -1,32 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { infoCircle } from "./icons";
import { onMount } from "svelte";
import Tooltip from "bootstrap/js/dist/tooltip";
export let html: string;
let ref: HTMLAnchorElement;
onMount(() => {
new Tooltip(ref, {
placement: "bottom",
html: true,
offset: [0, 20],
});
});
</script>
<span bind:this={ref} title={html}>
{@html infoCircle}
</span>
<style>
span :global(svg) {
vertical-align: text-bottom;
opacity: 0.3;
}
</style>

View file

@ -0,0 +1,20 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
let forId: string;
export { forId as for };
const dispatch = createEventDispatcher();
let spanRef: HTMLSpanElement;
onMount(() => {
dispatch("mount", { span: spanRef });
});
</script>
<label bind:this={spanRef} for={forId}><slot /></label>

View file

@ -4,12 +4,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte"; import TitledContainer from "./TitledContainer.svelte";
import EnumSelector from "./EnumSelector.svelte"; import Item from "components/Item.svelte";
import StepsInput from "./StepsInput.svelte"; import StepsInputRow from "./StepsInputRow.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte";
import EnumSelectorRow from "./EnumSelectorRow.svelte";
import Warning from "./Warning.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api = {};
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
@ -27,39 +32,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()]; const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
</script> </script>
<div> <TitledContainer title={tr.schedulingLapses()} {api}>
<h2>{tr.schedulingLapses()}</h2> <Item>
<StepsInputRow
bind:value={$config.relearnSteps}
defaultValue={defaults.relearnSteps}
markdownTooltip={tr.deckConfigRelearningStepsTooltip()}
>
{tr.deckConfigRelearningSteps()}
</StepsInputRow>
</Item>
<StepsInput <Item>
label={tr.deckConfigRelearningSteps()} <SpinBoxRow
tooltip={tr.deckConfigRelearningStepsTooltip()} bind:value={$config.minimumLapseInterval}
defaultValue={defaults.relearnSteps} defaultValue={defaults.minimumLapseInterval}
value={$config.relearnSteps} min={1}
on:changed={(evt) => ($config.relearnSteps = evt.detail.value)} markdownTooltip={tr.deckConfigMinimumIntervalTooltip()}
/> >
{tr.schedulingMinimumInterval()}
</SpinBoxRow>
<SpinBox <Warning warning={stepsExceedMinimumInterval} />
label={tr.schedulingMinimumInterval()} </Item>
tooltip={tr.deckConfigMinimumIntervalTooltip()}
warnings={[stepsExceedMinimumInterval]}
min={1}
defaultValue={defaults.minimumLapseInterval}
bind:value={$config.minimumLapseInterval}
/>
<SpinBox <Item>
label={tr.schedulingLeechThreshold()} <SpinBoxRow
tooltip={tr.deckConfigLeechThresholdTooltip()} bind:value={$config.leechThreshold}
min={1} defaultValue={defaults.leechThreshold}
defaultValue={defaults.leechThreshold} min={1}
bind:value={$config.leechThreshold} markdownTooltip={tr.deckConfigLeechThresholdTooltip()}
/> >
{tr.schedulingLeechThreshold()}
</SpinBoxRow>
</Item>
<EnumSelector <Item>
label={tr.schedulingLeechAction()} <EnumSelectorRow
tooltip={tr.deckConfigLeechActionTooltip()} bind:value={$config.leechAction}
choices={leechChoices} defaultValue={defaults.leechAction}
defaultValue={defaults.leechAction} choices={leechChoices}
bind:value={$config.leechAction} breakpoint="sm"
/> markdownTooltip={tr.deckConfigLeechActionTooltip()}
</div> >
{tr.schedulingLeechAction()}
</EnumSelectorRow>
</Item>
</TitledContainer>

View file

@ -4,12 +4,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte"; import TitledContainer from "./TitledContainer.svelte";
import StepsInput from "./StepsInput.svelte"; import Item from "components/Item.svelte";
import EnumSelector from "./EnumSelector.svelte"; import StepsInputRow from "./StepsInputRow.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte";
import EnumSelectorRow from "./EnumSelectorRow.svelte";
import Warning from "./Warning.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState; export let state: DeckOptionsState;
export let api = {};
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
@ -35,36 +40,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: ""; : "";
</script> </script>
<h2>{tr.schedulingNewCards()}</h2> <TitledContainer title={tr.schedulingNewCards()} {api}>
<Item>
<StepsInputRow
bind:value={$config.learnSteps}
defaultValue={defaults.learnSteps}
markdownTooltip={tr.deckConfigLearningStepsTooltip()}
>
{tr.deckConfigLearningSteps()}
</StepsInputRow>
</Item>
<StepsInput <Item>
label={tr.deckConfigLearningSteps()} <SpinBoxRow
tooltip={tr.deckConfigLearningStepsTooltip()} bind:value={$config.graduatingIntervalGood}
defaultValue={defaults.learnSteps} defaultValue={defaults.graduatingIntervalGood}
value={$config.learnSteps} markdownTooltip={tr.deckConfigGraduatingIntervalTooltip()}
on:changed={(evt) => ($config.learnSteps = evt.detail.value)} >
/> {tr.schedulingGraduatingInterval()}
</SpinBoxRow>
<SpinBox <Warning warning={stepsExceedGraduatingInterval} />
label={tr.schedulingGraduatingInterval()} </Item>
tooltip={tr.deckConfigGraduatingIntervalTooltip()}
warnings={[stepsExceedGraduatingInterval]}
defaultValue={defaults.graduatingIntervalGood}
bind:value={$config.graduatingIntervalGood}
/>
<SpinBox <Item>
label={tr.schedulingEasyInterval()} <SpinBoxRow
tooltip={tr.deckConfigEasyIntervalTooltip()} bind:value={$config.graduatingIntervalEasy}
warnings={[goodExceedsEasy]} defaultValue={defaults.graduatingIntervalEasy}
defaultValue={defaults.graduatingIntervalEasy} markdownTooltip={tr.deckConfigEasyIntervalTooltip()}
bind:value={$config.graduatingIntervalEasy} >
/> {tr.schedulingEasyInterval()}
</SpinBoxRow>
<EnumSelector <Warning warning={goodExceedsEasy} />
label={tr.deckConfigNewInsertionOrder()} </Item>
tooltip={tr.deckConfigNewInsertionOrderTooltip()}
choices={newInsertOrderChoices} <Item>
defaultValue={defaults.newCardInsertOrder} <EnumSelectorRow
bind:value={$config.newCardInsertOrder} bind:value={$config.newCardInsertOrder}
/> defaultValue={defaults.newCardInsertOrder}
choices={newInsertOrderChoices}
breakpoint={"md"}
markdownTooltip={tr.deckConfigNewInsertionOrderTooltip()}
>
{tr.deckConfigNewInsertionOrder()}
</EnumSelectorRow>
</Item>
</TitledContainer>

View file

@ -4,68 +4,84 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import { revertIcon } from "./icons"; import WithDropdownMenu from "components/WithDropdownMenu.svelte";
import { createEventDispatcher } from "svelte"; import DropdownMenu from "components/DropdownMenu.svelte";
import DropdownItem from "components/DropdownItem.svelte";
import Badge from "./Badge.svelte";
import { gearIcon, revertIcon } from "./icons";
import { isEqual as isEqualLodash, cloneDeep } from "lodash-es"; import { isEqual as isEqualLodash, cloneDeep } from "lodash-es";
// import { onMount } from "svelte"; import { touchDeviceKey } from "components/contextKeys";
// import Tooltip from "bootstrap/js/dist/tooltip"; import { getContext } from "svelte";
let ref: HTMLDivElement; type T = unknown;
// fixme: figure out why this breaks halfway down the page export let value: T;
// onMount(() => { export let defaultValue: T;
// new Tooltip(ref, {
// placement: "bottom",
// html: true,
// offset: [0, 20],
// });
// });
export let value: any; function isEqual(a: T, b: T): boolean {
export let defaultValue: any;
const dispatch = createEventDispatcher();
function isEqual(a: unknown, b: unknown): boolean {
if (typeof a === "number" && typeof b === "number") { if (typeof a === "number" && typeof b === "number") {
// round to .01 precision before comparing, // round to .01 precision before comparing,
// so the values coming out of the UI match // so the values coming out of the UI match
// the originals // the originals
return isEqualLodash(Math.round(a * 100) / 100, Math.round(b * 100) / 100); a = Math.round(a * 100) / 100;
} else { b = Math.round(b * 100) / 100;
return isEqualLodash(a, b);
} }
return isEqualLodash(a, b);
} }
let modified: boolean; let modified: boolean;
$: modified = !isEqual(value, defaultValue); $: modified = !isEqual(value, defaultValue);
$: className = !modified ? "opacity-25" : "";
const isTouchDevice = getContext<boolean>(touchDeviceKey);
/// This component can be used either with bind:value, or by listening
/// to the revert event.
function revert(): void { function revert(): void {
value = cloneDeep(defaultValue); value = cloneDeep(defaultValue);
dispatch("revert", { value });
} }
</script> </script>
{#if modified} <WithDropdownMenu
<div disabled={!modified}
class="img-div" let:createDropdown
on:click={revert} let:activateDropdown
bind:this={ref} let:menuId
title={tr.deckConfigRevertButtonTooltip()} >
<Badge
class={`p-1 ${className}`}
on:mount={(event) => createDropdown(event.detail.span)}
on:click={activateDropdown}
> >
{@html revertIcon} {@html gearIcon}
</div> </Badge>
{/if}
<DropdownMenu id={menuId}>
<DropdownItem
class={`spinner ${isTouchDevice ? "spin-always" : ""}`}
on:click={() => {
revert();
// Otherwise the menu won't close when the item is clicked
// TODO: investigate why this is necessary
activateDropdown();
}}
>
{tr.deckConfigRevertButtonTooltip()}<Badge>{@html revertIcon}</Badge>
</DropdownItem>
</DropdownMenu>
</WithDropdownMenu>
<style lang="scss"> <style lang="scss">
.img-div { :global(.spinner:hover .badge, .spinner.spin-always .badge) {
display: flex; animation: spin-animation 1s infinite;
animation-timing-function: linear;
}
:global(svg) { @keyframes -global-spin-animation {
align-self: center; 0% {
opacity: 0.3; transform: rotate(360deg);
}
100% {
transform: rotate(0deg);
} }
} }
</style> </style>

11
ts/deckoptions/Row.svelte Normal file
View file

@ -0,0 +1,11 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
export let id: string | undefined = undefined;
</script>
<div {id} class="row gx-0 gy-2 mt-0">
<slot />
</div>

View file

@ -69,7 +69,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonGroupItem> <ButtonGroupItem>
<WithDropdownMenu let:createDropdown let:activateDropdown let:menuId> <WithDropdownMenu let:createDropdown let:activateDropdown let:menuId>
<LabelButton on:mount={createDropdown} on:click={activateDropdown} /> <LabelButton
on:mount={(event) => createDropdown(event.detail.button)}
on:click={activateDropdown}
/>
<DropdownMenu id={menuId}> <DropdownMenu id={menuId}>
<DropdownItem on:click={() => dispatch("add")} <DropdownItem on:click={() => dispatch("add")}
>{tr.deckConfigAddGroup()}</DropdownItem >{tr.deckConfigAddGroup()}</DropdownItem

View file

@ -3,15 +3,14 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import ConfigEntry from "./ConfigEntry.svelte"; import { getContext } from "svelte";
import { nightModeKey } from "components/contextKeys";
export let label: string;
export let tooltip: string;
export let value: number; export let value: number;
export let min = 1; export let min = 1;
export let max = 9999; export let max = 9999;
export let warnings: string[] = [];
export let defaultValue: number = 0; const nightMode = getContext<boolean>(nightModeKey);
function checkMinMax() { function checkMinMax() {
if (value > max) { if (value > max) {
@ -22,13 +21,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
</script> </script>
<ConfigEntry {label} {tooltip} {warnings} bind:value {defaultValue}> <input
<input type="number"
type="number" pattern="[0-9]*"
{min} inputmode="numeric"
{max} {min}
bind:value {max}
class="form-control" bind:value
on:blur={checkMinMax} class="form-control"
/> class:nightMode
</ConfigEntry> on:blur={checkMinMax}
/>
<style lang="scss">
@use "ts/sass/night_mode" as nightmode;
.nightMode {
@include nightmode.input;
}
</style>

View file

@ -3,41 +3,40 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import ConfigEntry from "./ConfigEntry.svelte"; import { nightModeKey } from "components/contextKeys";
import type { NumberValueEvent } from "./events";
export let label: string;
export let tooltip: string;
export let value: number; export let value: number;
export let defaultValue: number;
export let min = 1; export let min = 1;
export let max = 9999; export let max = 9999;
const dispatch = createEventDispatcher();
let stringValue: string; let stringValue: string;
$: stringValue = value.toFixed(2); $: stringValue = value.toFixed(2);
function update(this: HTMLInputElement): void { const nightMode = getContext<boolean>(nightModeKey);
dispatch("changed", {
value: Math.min(max, Math.max(min, parseFloat(this.value))),
});
}
function revert(evt: NumberValueEvent): void { function update(this: HTMLInputElement): void {
dispatch("changed", { value: evt.detail.value }); value = Math.min(max, Math.max(min, parseFloat(this.value)));
} }
</script> </script>
<ConfigEntry {label} {tooltip} {value} {defaultValue} on:revert={revert}> <input
<input type="number"
type="number" pattern="[0-9]*"
{min} inputmode="decimal"
{max} class="form-control"
step="0.01" class:nightMode
value={stringValue} {min}
on:blur={update} {max}
class="form-control" step="0.01"
/> value={stringValue}
</ConfigEntry> on:blur={update}
/>
<style lang="scss">
@use "ts/sass/night_mode" as nightmode;
.nightMode {
@include nightmode.input;
}
</style>

View file

@ -0,0 +1,27 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import SpinBoxFloat from "./SpinBoxFloat.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: any;
export let defaultValue: any;
export let min = 0;
export let max: number | undefined = undefined;
export let markdownTooltip: string;
</script>
<Row>
<Col size={7}>
<RevertButton bind:value {defaultValue} />
<TooltipLabel {markdownTooltip}><slot /></TooltipLabel>
</Col>
<Col size={5}>
<SpinBoxFloat bind:value {min} {max} />
</Col>
</Row>

View file

@ -0,0 +1,27 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import SpinBox from "./SpinBox.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: any;
export let defaultValue: any;
export let min = 0;
export let max: number | undefined = undefined;
export let markdownTooltip: string;
</script>
<Row>
<Col size={7}>
<RevertButton bind:value {defaultValue} />
<TooltipLabel {markdownTooltip}><slot /></TooltipLabel>
</Col>
<Col size={5}>
<SpinBox bind:value {min} {max} />
</Col>
</Row>

View file

@ -3,40 +3,34 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import { nightModeKey } from "components/contextKeys";
import { stepsToString, stringToSteps } from "./steps"; import { stepsToString, stringToSteps } from "./steps";
import ConfigEntry from "./ConfigEntry.svelte";
import type { NumberValueEvent } from "./events";
export let label: string;
export let tooltip: string;
export let value: number[]; export let value: number[];
export let defaultValue: number[];
export let warnings: string[] = [];
const dispatch = createEventDispatcher();
let stringValue: string; let stringValue: string;
$: stringValue = stepsToString(value); $: stringValue = stepsToString(value);
function update(this: HTMLInputElement): void { const nightMode = getContext<boolean>(nightModeKey);
const value = stringToSteps(this.value);
dispatch("changed", { value });
}
function revert(evt: NumberValueEvent): void { function update(this: HTMLInputElement): void {
dispatch("changed", { value: evt.detail.value }); value = stringToSteps(this.value);
} }
</script> </script>
<ConfigEntry <input
{label} type="text"
{tooltip} value={stringValue}
{value} class="form-control"
{defaultValue} class:nightMode
{warnings} on:blur={update}
wholeLine={value.length > 2} />
on:revert={revert}
> <style lang="scss">
<input type="text" value={stringValue} on:blur={update} class="form-control" /> @use "ts/sass/night_mode" as nightmode;
</ConfigEntry>
.nightMode {
@include nightmode.input;
}
</style>

View file

@ -0,0 +1,25 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import StepsInput from "./StepsInput.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: any;
export let defaultValue: any;
export let markdownTooltip: string;
</script>
<Row>
<Col size={7}>
<RevertButton bind:value {defaultValue} />
<TooltipLabel {markdownTooltip}><slot /></TooltipLabel>
</Col>
<Col size={5}>
<StepsInput bind:value />
</Col>
</Row>

View file

@ -0,0 +1,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 { getContext } from "svelte";
import { nightModeKey } from "components/contextKeys";
export let id: string | undefined;
export let value: boolean;
export let disabled = false;
const nightMode = getContext<boolean>(nightModeKey);
</script>
<div class="form-check form-switch">
<input
{id}
type="checkbox"
class="form-check-input"
class:nightMode
bind:checked={value}
{disabled}
/>
</div>
<style lang="scss">
.form-switch {
/* bootstrap adds a default 2.5em left pad, which causes */
/* text to wrap prematurely */
padding-left: 0.5em;
}
.form-check-input {
-webkit-appearance: none;
height: 1.6em;
/* otherwise the switch circle shows slightly off-centered */
margin-top: 0;
.form-switch & {
width: 3em;
margin-left: 1.5em;
}
}
.nightMode:not(:checked) {
background-color: var(--frame-bg);
border-color: var(--border);
}
</style>

View file

@ -0,0 +1,30 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Row from "./Row.svelte";
import Col from "./Col.svelte";
import Label from "./Label.svelte";
import TooltipLabel from "./TooltipLabel.svelte";
import Switch from "./Switch.svelte";
import RevertButton from "./RevertButton.svelte";
export let value: boolean;
export let defaultValue: boolean;
export let markdownTooltip: string | undefined = undefined;
const id = Math.random().toString(36).substring(2);
</script>
<Row>
<Col>
<RevertButton bind:value {defaultValue} />
{#if markdownTooltip}<TooltipLabel for={id} {markdownTooltip}
><slot /></TooltipLabel
>{:else}<Label for={id}><slot /></Label>{/if}
</Col>
<Col grow={false}>
<Switch {id} bind:value />
</Col>
</Row>

View file

@ -0,0 +1,42 @@
<!--
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 "lib/i18n";
import TitledContainer from "./TitledContainer.svelte";
import Item from "components/Item.svelte";
import SpinBoxRow from "./SpinBoxRow.svelte";
import SwitchRow from "./SwitchRow.svelte";
import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState;
export let api: Record<string, never>;
let config = state.currentConfig;
let defaults = state.defaults;
</script>
<TitledContainer title={tr.deckConfigTimerTitle()} {api}>
<Item>
<SpinBoxRow
bind:value={$config.capAnswerTimeToSecs}
defaultValue={defaults.capAnswerTimeToSecs}
min={30}
max={600}
markdownTooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
>
{tr.deckConfigMaximumAnswerSecs()}
</SpinBoxRow>
</Item>
<Item>
<SwitchRow
bind:value={$config.showTimer}
defaultValue={defaults.showTimer}
markdownTooltip={tr.deckConfigShowAnswerTimerTooltip()}
>
{tr.schedulingShowAnswerTimer()}
</SwitchRow>
</Item>
</TitledContainer>

View file

@ -0,0 +1,24 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Section from "components/Section.svelte";
export let title: string;
export let api: Record<string, never> | undefined = undefined;
</script>
<div class="container-fluid my-4">
<h1>{title}</h1>
<Section {api}>
<slot />
</Section>
</div>
<style lang="scss">
h1 {
border-bottom: 1px solid var(--medium-border);
}
</style>

View file

@ -0,0 +1,27 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import marked from "marked";
import { infoCircle } from "./icons";
import WithTooltip from "./WithTooltip.svelte";
import Label from "./Label.svelte";
import Badge from "./Badge.svelte";
export let markdownTooltip: string;
let forId: string;
export { forId as for };
</script>
<span>
<Label for={forId}><slot /></Label>
<WithTooltip tooltip={marked(markdownTooltip)} let:createTooltip>
<Badge
class="opacity-50"
on:mount={(event) => createTooltip(event.detail.span)}
>
{@html infoCircle}
</Badge>
</WithTooltip>
</span>

View file

@ -0,0 +1,18 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { slide } from "svelte/transition";
import Row from "./Row.svelte";
export let warning: string;
</script>
{#if warning}
<Row>
<div class="col-12 alert alert-warning mb-0" in:slide out:slide>
{warning}
</div>
</Row>
{/if}

View file

@ -0,0 +1,42 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onDestroy } from "svelte";
import Tooltip from "bootstrap/js/dist/tooltip";
type TriggerType =
| "hover focus"
| "click"
| "hover"
| "focus"
| "manual"
| "click hover"
| "click focus"
| "click hover focus";
export let tooltip: string;
export let trigger: TriggerType = "hover focus";
let tooltipObject: Tooltip;
function createTooltip(element: HTMLElement): void {
element.title = tooltip;
tooltipObject = new Tooltip(element, {
placement: "bottom",
html: true,
offset: [0, 20],
delay: { show: 250, hide: 0 },
trigger,
});
}
onDestroy(() => {
if (tooltipObject) {
tooltipObject.dispose();
}
});
</script>
<slot {createTooltip} {tooltipObject} />

View file

@ -1,4 +1,4 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export type Size = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type NumberValueEvent = { detail: { value: number } }; export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl";

View file

@ -1,59 +1,46 @@
$tooltip-padding-y: 0.45rem;
$tooltip-padding-x: 0.65rem;
$tooltip-max-width: 300px;
@use "ts/sass/vars"; @use "ts/sass/vars";
@use "ts/sass/scrollbar"; @use "ts/sass/scrollbar";
@use "ts/sass/bootstrap-dark";
@import "ts/sass/base"; @import "ts/sass/base";
@import "ts/sass/bootstrap/containers";
@import "ts/sass/bootstrap/grid";
@import "ts/sass/bootstrap/dropdown"; @import "ts/sass/bootstrap/dropdown";
@import "ts/sass/bootstrap/forms"; @import "ts/sass/bootstrap/forms";
@import "ts/sass/bootstrap/buttons"; @import "ts/sass/bootstrap/buttons";
@import "ts/sass/bootstrap/button-group"; @import "ts/sass/bootstrap/button-group";
@import "ts/sass/bootstrap/transitions";
@import "ts/sass/bootstrap/modal"; @import "ts/sass/bootstrap/modal";
@import "ts/sass/bootstrap/close"; @import "ts/sass/bootstrap/close";
@import "ts/sass/bootstrap/alert"; @import "ts/sass/bootstrap/alert";
@import "ts/sass/bootstrap/tooltip"; @import "ts/sass/bootstrap/tooltip";
@import "ts/sass/bootstrap/badge";
.night-mode { .night-mode {
@include scrollbar.night-mode; @include scrollbar.night-mode;
@include bootstrap-dark.night-mode;
} }
// the unprefixed version wasn't added until Chrome 81 .form-control,
.form-select { .form-select {
// the unprefixed version wasn't added until Chrome 81
-webkit-appearance: none; -webkit-appearance: none;
} }
body {
width: min(100vw, 35em);
margin: 0 auto;
// leave some space for rounded screens
margin-bottom: 2em;
}
html {
overflow-x: hidden;
}
#main {
padding: 0.5em;
padding-top: 0;
}
.tooltip-inner { .tooltip-inner {
max-width: 300px;
text-align: left; text-align: left;
p { // marked transpiles tooltips into multiple paragraphs
margin: 0.8em 0.4em 0.8em 0.8em; // where trailing <p>s cause a bottom margin
> p:last-child {
display: inline;
}
// the default code color in tooltips is difficult to read; we'll probably
// want to add more of our own styling in the future
code {
color: #ffaaaa;
} }
} }
// the default code color in tooltips is difficult to read; we'll probably
// want to add more of our own styling in the future
code {
color: #ffaaaa;
}
// override the default down arrow colour in <select> elements
.night-mode select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
}

View file

@ -3,7 +3,6 @@
// Import icons from bootstrap // Import icons from bootstrap
import revertIcon from "./arrow-counterclockwise.svg"; export { default as revertIcon } from "./arrow-counterclockwise.svg";
import infoCircle from "./info-circle.svg"; export { default as infoCircle } from "./info-circle.svg";
export { default as gearIcon } from "./gear.svg";
export { revertIcon, infoCircle };

View file

@ -14,7 +14,7 @@ import SpinBoxFloat from "./SpinBoxFloat.svelte";
import EnumSelector from "./EnumSelector.svelte"; import EnumSelector from "./EnumSelector.svelte";
import CheckBox from "./CheckBox.svelte"; import CheckBox from "./CheckBox.svelte";
import { nightModeKey, modalsKey } from "components/contextKeys"; import { nightModeKey, touchDeviceKey, modalsKey } from "components/contextKeys";
export async function deckOptions( export async function deckOptions(
target: HTMLDivElement, target: HTMLDivElement,
@ -31,13 +31,16 @@ export async function deckOptions(
}), }),
]); ]);
const nightMode = checkNightMode();
const context = new Map(); const context = new Map();
const nightMode = checkNightMode();
context.set(nightModeKey, nightMode); context.set(nightModeKey, nightMode);
const modals = new Map(); const modals = new Map();
context.set(modalsKey, modals); context.set(modalsKey, modals);
const touchDevice = "ontouchstart" in document.documentElement;
context.set(touchDeviceKey, touchDevice);
const state = new DeckOptionsState(deckId, info); const state = new DeckOptionsState(deckId, info);
return new DeckOptionsPage({ return new DeckOptionsPage({
target, target,

View file

@ -33,7 +33,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { isApplePlatform } from "lib/platform"; import { isApplePlatform } from "lib/platform";
import StickyBar from "components/StickyBar.svelte"; import StickyBar from "components/StickyBar.svelte";
import ButtonToolbar from "components/ButtonToolbar.svelte"; import ButtonToolbar from "components/ButtonToolbar.svelte";
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; import Item from "components/Item.svelte";
import NoteTypeButtons from "./NoteTypeButtons.svelte"; import NoteTypeButtons from "./NoteTypeButtons.svelte";
import FormatInlineButtons from "./FormatInlineButtons.svelte"; import FormatInlineButtons from "./FormatInlineButtons.svelte";
@ -41,37 +41,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ColorButtons from "./ColorButtons.svelte"; import ColorButtons from "./ColorButtons.svelte";
import TemplateButtons from "./TemplateButtons.svelte"; import TemplateButtons from "./TemplateButtons.svelte";
export let size = isApplePlatform() ? 1.6 : 2.0;
export let wrap = true;
export const toolbar = {}; export const toolbar = {};
export const notetypeButtons = {}; export const notetypeButtons = {};
export const formatInlineButtons = {}; export const formatInlineButtons = {};
export const formatBlockButtons = {}; export const formatBlockButtons = {};
export const colorButtons = {}; export const colorButtons = {};
export const templateButtons = {}; export const templateButtons = {};
export let size = isApplePlatform() ? 1.6 : 2.0;
export let wrap = true;
</script> </script>
<StickyBar> <StickyBar>
<ButtonToolbar {size} {wrap} api={toolbar}> <ButtonToolbar {size} {wrap} api={toolbar}>
<ButtonToolbarItem id="notetype"> <Item id="notetype">
<NoteTypeButtons api={notetypeButtons} /> <NoteTypeButtons api={notetypeButtons} />
</ButtonToolbarItem> </Item>
<ButtonToolbarItem id="inlineFormatting"> <Item id="inlineFormatting">
<FormatInlineButtons api={formatInlineButtons} /> <FormatInlineButtons api={formatInlineButtons} />
</ButtonToolbarItem> </Item>
<ButtonToolbarItem id="blockFormatting"> <Item id="blockFormatting">
<FormatBlockButtons api={formatBlockButtons} /> <FormatBlockButtons api={formatBlockButtons} />
</ButtonToolbarItem> </Item>
<ButtonToolbarItem id="color"> <Item id="color">
<ColorButtons api={colorButtons} /> <ColorButtons api={colorButtons} />
</ButtonToolbarItem> </Item>
<ButtonToolbarItem id="template"> <Item id="template">
<TemplateButtons api={templateButtons} /> <TemplateButtons api={templateButtons} />
</ButtonToolbarItem> </Item>
</ButtonToolbar> </ButtonToolbar>
</StickyBar> </StickyBar>

View file

@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonGroupItem from "components/ButtonGroupItem.svelte"; import ButtonGroupItem from "components/ButtonGroupItem.svelte";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import ButtonDropdown from "components/ButtonDropdown.svelte"; import ButtonDropdown from "components/ButtonDropdown.svelte";
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; import Item from "components/Item.svelte";
import WithDropdownMenu from "components/WithDropdownMenu.svelte"; import WithDropdownMenu from "components/WithDropdownMenu.svelte";
import OnlyEditable from "./OnlyEditable.svelte"; import OnlyEditable from "./OnlyEditable.svelte";
import CommandIconButton from "./CommandIconButton.svelte"; import CommandIconButton from "./CommandIconButton.svelte";
@ -71,7 +71,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</OnlyEditable> </OnlyEditable>
<ButtonDropdown id={menuId}> <ButtonDropdown id={menuId}>
<ButtonToolbarItem id="justify"> <Item id="justify">
<ButtonGroup> <ButtonGroup>
<ButtonGroupItem> <ButtonGroupItem>
<CommandIconButton <CommandIconButton
@ -109,9 +109,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
> >
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </Item>
<ButtonToolbarItem id="indentation"> <Item id="indentation">
<ButtonGroup> <ButtonGroup>
<ButtonGroupItem> <ButtonGroupItem>
<OnlyEditable let:disabled> <OnlyEditable let:disabled>
@ -137,7 +137,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</OnlyEditable> </OnlyEditable>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </Item>
</ButtonDropdown> </ButtonDropdown>
</WithDropdownMenu> </WithDropdownMenu>
</ButtonGroupItem> </ButtonGroupItem>

View file

@ -53,6 +53,14 @@ sass_library(
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
sass_library(
name = "night_mode_lib",
srcs = [
"night_mode.scss",
],
visibility = ["//visibility:public"],
)
exports_files( exports_files(
["_vars.scss"], ["_vars.scss"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],

View file

@ -6,6 +6,19 @@ $body-bg: var(--window-bg);
$link-hover-color: var(--link); $link-hover-color: var(--link);
$link-hover-decoration: none; $link-hover-decoration: none;
$utilities: (
"opacity": (
property: opacity,
values: (
0: 0,
25: 0.25,
50: 0.5,
75: 0.75,
100: 1,
),
),
);
@import "ts/sass/bootstrap/bootstrap-reboot"; @import "ts/sass/bootstrap/bootstrap-reboot";
@import "ts/sass/bootstrap/bootstrap-utilities"; @import "ts/sass/bootstrap/bootstrap-utilities";

View file

@ -116,3 +116,7 @@ $focus-color: $blue;
box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5) box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5)
rgba(black, $intensity); rgba(black, $intensity);
} }
@function down-arrow($color) {
@return url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='transparent' stroke='#{$color}' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
}

11
ts/sass/night_mode.scss Normal file
View file

@ -0,0 +1,11 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@mixin input {
background-color: var(--frame-bg);
border-color: var(--border);
&:focus {
background-color: var(--window-bg);
}
}