Skip to content
128 changes: 73 additions & 55 deletions frontend/src/ts/components/pages/settings/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
showErrorNotification,
showSuccessNotification,
} from "../../../states/notifications";
import { getSettingsSearch } from "../../../states/settings-search";
import { cn } from "../../../utils/cn";
import { Button } from "../../common/Button";
import { FaProps } from "../../common/Fa";
Expand All @@ -18,67 +19,84 @@ type Props = {
description: string | JSXElement;
inputs?: JSXElement;
fullWidthInputs?: JSXElement;
// extra text (e.g. option labels) the search filter also matches against
searchTerms?: string;
};

export function Setting(props: Props): JSXElement {
const matchesSearch = (): boolean => {
const query = getSettingsSearch().trim().toLowerCase();
if (query === "") return true;
const haystack = [
props.title,
typeof props.description === "string" ? props.description : "",
props.searchTerms ?? "",
]
.join(" ")
.toLowerCase();
return haystack.includes(query);
};

return (
<div
class={cn(
"group grid gap-2",
"-m-4 rounded-double p-4",
// "animate-[ring-flash_4s_ease-in_forwards]",
)}
data-setting-key={props.key}
>
<div class="flex gap-2">
<H3 text={props.title} fa={props.fa} class="pb-0" />
<Button
class="-m-2 p-2 opacity-0 group-hover:opacity-100"
variant="text"
fa={{ icon: "fa-link" }}
onClick={() => {
const urlParams = serialize({
schema: z.object({
highlight: z.string(),
}),
data: {
highlight: props.key,
},
});
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
window.history.replaceState({}, "", newUrl);
<Show when={matchesSearch()}>
<div
class={cn(
"group grid gap-2",
"-m-4 rounded-double p-4",
// "animate-[ring-flash_4s_ease-in_forwards]",
)}
data-setting-key={props.key}
>
<div class="flex gap-2">
<H3 text={props.title} fa={props.fa} class="pb-0" />
<Button
class="-m-2 p-2 opacity-0 group-hover:opacity-100"
variant="text"
fa={{ icon: "fa-link" }}
onClick={() => {
const urlParams = serialize({
schema: z.object({
highlight: z.string(),
}),
data: {
highlight: props.key,
},
});
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
window.history.replaceState({}, "", newUrl);

navigator.clipboard
.writeText(window.location.toString())
.then(() => {
showSuccessNotification("Link copied to clipboard");
})
.catch((e: unknown) => {
showErrorNotification("Failed to copy to clipboard", {
error: e,
navigator.clipboard
.writeText(window.location.toString())
.then(() => {
showSuccessNotification("Link copied to clipboard");
})
.catch((e: unknown) => {
showErrorNotification("Failed to copy to clipboard", {
error: e,
});
});
});
}}
/>
</div>
<Show when={props.inputs !== undefined}>
<div
class={cn(
"grid grid-cols-1 gap-2",
"md:grid-cols-[1fr_1fr] md:gap-x-8",
"lg:grid-cols-[1.5fr_1fr]",
"xl:grid-cols-[2fr_1fr]",
props.inputs === undefined &&
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-1",
)}
>
<Show when={props.description !== ""}>
<div class="">{props.description}</div>
</Show>
<div>{props.inputs}</div>
}}
/>
</div>
</Show>
<Show when={props.fullWidthInputs}>{props.fullWidthInputs}</Show>
</div>
<Show when={props.inputs !== undefined}>
<div
class={cn(
"grid grid-cols-1 gap-2",
"md:grid-cols-[1fr_1fr] md:gap-x-8",
"lg:grid-cols-[1.5fr_1fr]",
"xl:grid-cols-[2fr_1fr]",
props.inputs === undefined &&
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-1",
)}
>
<Show when={props.description !== ""}>
<div class="">{props.description}</div>
</Show>
<div>{props.inputs}</div>
</div>
</Show>
<Show when={props.fullWidthInputs}>{props.fullWidthInputs}</Show>
</div>
</Show>
);
}
119 changes: 64 additions & 55 deletions frontend/src/ts/components/pages/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { z } from "zod";

import { resetConfig } from "../../../config/lifecycle";
import { configMetadata, OptionMetadata } from "../../../config/metadata";
import {
getOptionLabel,
getOptionSearchTerms,
} from "../../../config/option-strings";
import { setConfig } from "../../../config/setters";
import { getConfig } from "../../../config/store";
import {
Expand All @@ -16,6 +20,7 @@ import { useLocalStorage } from "../../../hooks/useLocalStorage";
import { useSavedIndicator } from "../../../hooks/useSavedIndicator";
import { isAuthenticated } from "../../../states/core";
import { showModal } from "../../../states/modals";
import { isSettingsSearchActive } from "../../../states/settings-search";
import { showSimpleModal } from "../../../states/simple-modal";
import { cn } from "../../../utils/cn";
import fileStorage from "../../../utils/file-storage";
Expand Down Expand Up @@ -52,6 +57,7 @@ import { Tags } from "./custom-setting/Tags";
import { Theme } from "./custom-setting/Theme";
import { QuickNav } from "./QuickNav";
import { Setting } from "./Setting";
import { SettingsSearch } from "./SettingsSearch";

export function SettingsPage(): JSXElement {
const [hasLocalBg] = createResource(
Expand All @@ -62,16 +68,23 @@ export function SettingsPage(): JSXElement {
return (
<Page id="settings">
<div class="grid gap-8">
<QuickNav />
<Show when={getConfig.showKeyTips}>
<SettingsSearch />
{/* while filtering, show only the matching settings (hide everything else) */}
<Show when={!isSettingsSearchActive()}>
<QuickNav />
</Show>
<Show when={getConfig.showKeyTips && !isSettingsSearchActive()}>
<div class="text-center text-sub">
tip: You can also change all these settings quickly using the
command line
<br />( <CommandlineHotkey /> )
</div>
</Show>
<AccountSettingsNotice />
<div>
<Show when={!isSettingsSearchActive()}>
<AccountSettingsNotice />
</Show>
{/* while filtering, lay the matching sections out with a uniform gap */}
<div class={cn(isSettingsSearchActive() && "grid gap-8")}>
<Section title="behavior">
<Show when={isAuthenticated()}>
<Tags />
Expand Down Expand Up @@ -248,7 +261,9 @@ export function SettingsPage(): JSXElement {
</Section>
</div>

<AccountSettingsNotice />
<Show when={!isSettingsSearchActive()}>
<AccountSettingsNotice />
</Show>
</div>
</Page>
);
Expand Down Expand Up @@ -287,25 +302,39 @@ function Section(props: { title: string; children: JSXElement }): JSXElement {
const [isOpen, setIsOpen] = createSignal(true);

return (
<div id={`group_${wordsToCamelCase(props.title)}`}>
<Button
variant="text"
class="mb-8 w-max gap-4 p-0 text-4xl"
onClick={() => setIsOpen((prev) => !prev)}
>
<Anime
animation={{
rotate: isOpen() ? 0 : -90,
duration: 125,
}}
<div
id={`group_${wordsToCamelCase(props.title)}`}
class={cn(
// when filtering, drop sections that have no matching setting
isSettingsSearchActive() && "not-has-[[data-setting-key]]:hidden",
)}
>
<Show when={!isSettingsSearchActive()}>
<Button
variant="text"
class="mb-8 w-max gap-4 p-0 text-4xl"
onClick={() => setIsOpen((prev) => !prev)}
>
<Fa icon="fa-chevron-down" />
</Anime>
{props.title}
</Button>
<AnimeShow when={isOpen()} slide class="grid gap-8">
<Anime
animation={{
rotate: isOpen() ? 0 : -90,
duration: 125,
}}
>
<Fa icon="fa-chevron-down" />
</Anime>
{props.title}
</Button>
</Show>
<AnimeShow
when={isOpen() || isSettingsSearchActive()}
slide
class="grid gap-8"
>
{props.children}
<div class="h-16"></div>
<Show when={!isSettingsSearchActive()}>
<div class="h-16"></div>
</Show>
</AnimeShow>
</div>
);
Expand Down Expand Up @@ -405,39 +434,18 @@ function AutoSetting<T extends keyof Config>(props: {
)}
>
<For each={options}>
{(option) => {
const text = () => {
const optionsMeta = configMetadata[props.key]
.optionsMetadata as
| Record<string, { displayString?: string }>
| undefined;
const match = optionsMeta?.[String(option)];
if (match?.displayString !== undefined) {
return match.displayString;
}

if (option === true) {
return "on";
}
if (option === false) {
return "off";
}

return (option as string).toString().replace(/_/g, " ");
};
return (
<Button
active={getConfig[props.key] === option}
onClick={() => {
if (getConfig[props.key] === option) return;
props.onOptionClick?.(option as Config[T]);
setConfig(props.key, option as Config[T]);
}}
>
{text()}
</Button>
);
}}
{(option) => (
<Button
active={getConfig[props.key] === option}
onClick={() => {
if (getConfig[props.key] === option) return;
props.onOptionClick?.(option as Config[T]);
setConfig(props.key, option as Config[T]);
}}
>
{getOptionLabel(props.key, option as Config[T])}
</Button>
)}
</For>
</div>
);
Expand All @@ -451,6 +459,7 @@ function AutoSetting<T extends keyof Config>(props: {
title={configMetadata[props.key].displayString ?? props.key}
fa={configMetadata[props.key].fa}
description={configMetadata[props.key].description}
searchTerms={getOptionSearchTerms(props.key)}
inputs={!props.wide ? autoInputs() : props.inputs}
fullWidthInputs={
props.wide ? (autoInputs() ?? props.inputs) : props.inputs
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/ts/components/pages/settings/SettingsSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { JSXElement, onCleanup, Show } from "solid-js";

import {
getSettingsSearch,
setSettingsSearch,
} from "../../../states/settings-search";
import { cn } from "../../../utils/cn";
import { Button } from "../../common/Button";
import { Fa } from "../../common/Fa";

export function SettingsSearch(): JSXElement {
// reset the filter when leaving the settings page
onCleanup(() => setSettingsSearch(""));

return (
<div class="relative">
<Fa
icon="fa-search"
class="pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-sub"
/>
<input
class={cn(
"w-full rounded border-none bg-sub-alt py-3 pr-10 pl-10",
"text-em-base text-text caret-main outline-none placeholder:text-sub",
"focus-visible:shadow-[0_0_0_0.1rem_var(--bg-color),0_0_0_0.2rem_var(--text-color)]",
)}
type="text"
placeholder="search settings"
autocomplete="off"
value={getSettingsSearch()}
onInput={(e) => setSettingsSearch(e.currentTarget.value)}
/>
<Show when={getSettingsSearch() !== ""}>
<Button
variant="text"
class="absolute top-1/2 right-2 -translate-y-1/2"
fa={{ icon: "fa-times" }}
balloon={{ text: "clear search", position: "left" }}
onClick={() => setSettingsSearch("")}
/>
</Show>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createForm } from "@tanstack/solid-form";
import { createResource, JSXElement, For, Show } from "solid-js";

import { configMetadata } from "../../../../config/metadata";
import { getOptionSearchTerms } from "../../../../config/option-strings";
import { setConfig } from "../../../../config/setters";
import { getConfig } from "../../../../config/store";
import { applyCustomBackground } from "../../../../controllers/theme-controller";
Expand Down Expand Up @@ -56,6 +57,7 @@ export function CustomBackground(): JSXElement {
configMetadata.customBackground.displayString ?? "custom background"
}
fa={configMetadata.customBackground.fa}
searchTerms={getOptionSearchTerms("customBackgroundSize")}
description={
<>
{configMetadata.customBackground.description}
Expand Down
Loading
Loading