mirror of
https://github.com/zadam/trilium.git
synced 2025-11-06 13:26:01 +01:00
feat(react/settings): port theme switch
This commit is contained in:
14
apps/client/src/widgets/react/Column.tsx
Normal file
14
apps/client/src/widgets/react/Column.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ComponentChildren } from "preact";
|
||||||
|
|
||||||
|
interface ColumnProps {
|
||||||
|
md: number;
|
||||||
|
children: ComponentChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Column({ md, children }: ColumnProps) {
|
||||||
|
return (
|
||||||
|
<div className={`col-md-${md}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
apps/client/src/widgets/react/FormSelect.tsx
Normal file
25
apps/client/src/widgets/react/FormSelect.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
interface FormSelectProps {
|
||||||
|
currentValue?: string;
|
||||||
|
onChange(newValue: string): void;
|
||||||
|
values: { val: string, title: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormSelect({ currentValue, values, onChange }: FormSelectProps) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||||
|
>
|
||||||
|
{values.map(item => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
value={item.val}
|
||||||
|
selected={item.val === currentValue}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,5 +28,5 @@ export function renderReactWidget(parentComponent: Component, el: JSX.Element) {
|
|||||||
{el}
|
{el}
|
||||||
</ParentComponent.Provider>
|
</ParentComponent.Provider>
|
||||||
), renderContainer);
|
), renderContainer);
|
||||||
return $(renderContainer.firstChild as HTMLElement);
|
return $(renderContainer.children) as JQuery<HTMLElement>;
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import { ParentComponent } from "./ReactBasicWidget";
|
|||||||
import SpacedUpdate from "../../services/spaced_update";
|
import SpacedUpdate from "../../services/spaced_update";
|
||||||
import { OptionNames } from "@triliumnext/commons";
|
import { OptionNames } from "@triliumnext/commons";
|
||||||
import options from "../../services/options";
|
import options from "../../services/options";
|
||||||
import utils from "../../services/utils";
|
import utils, { reloadFrontendApp } from "../../services/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
|
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
|
||||||
@@ -68,12 +68,16 @@ export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000)
|
|||||||
return spacedUpdateRef.current;
|
return spacedUpdateRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTriliumOption(name: OptionNames): [string, (newValue: string) => Promise<void>] {
|
export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: string) => Promise<void>] {
|
||||||
const initialValue = options.get(name);
|
const initialValue = options.get(name);
|
||||||
const [ value, setValue ] = useState(initialValue);
|
const [ value, setValue ] = useState(initialValue);
|
||||||
|
|
||||||
async function wrappedSetValue(newValue: string) {
|
async function wrappedSetValue(newValue: string) {
|
||||||
await options.save(name, newValue);
|
await options.save(name, newValue);
|
||||||
|
|
||||||
|
if (needsRefresh) {
|
||||||
|
reloadFrontendApp(`option change: ${name}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
|||||||
@@ -1,31 +1,68 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import { isMobile, reloadFrontendApp } from "../../../services/utils";
|
import { isMobile, reloadFrontendApp } from "../../../services/utils";
|
||||||
|
import Column from "../../react/Column";
|
||||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||||
|
import FormSelect from "../../react/FormSelect";
|
||||||
import { useTriliumOption } from "../../react/hooks";
|
import { useTriliumOption } from "../../react/hooks";
|
||||||
import OptionsSection from "./components/OptionsSection";
|
import OptionsSection from "./components/OptionsSection";
|
||||||
|
import server from "../../../services/server";
|
||||||
|
|
||||||
|
interface Theme {
|
||||||
|
val: string;
|
||||||
|
title: string;
|
||||||
|
noteId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILTIN_THEMES: Theme[] = [
|
||||||
|
{ val: "next", title: t("theme.triliumnext") },
|
||||||
|
{ val: "next-light", title: t("theme.triliumnext-light") },
|
||||||
|
{ val: "next-dark", title: t("theme.triliumnext-dark") },
|
||||||
|
{ val: "auto", title: t("theme.auto_theme") },
|
||||||
|
{ val: "light", title: t("theme.light_theme") },
|
||||||
|
{ val: "dark", title: t("theme.dark_theme") }
|
||||||
|
]
|
||||||
|
|
||||||
export default function AppearanceSettings() {
|
export default function AppearanceSettings() {
|
||||||
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation");
|
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true);
|
||||||
|
const [ theme, setTheme ] = useTriliumOption("theme", true);
|
||||||
|
|
||||||
|
const [ themes, setThemes ] = useState<Theme[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
server.get<Theme[]>("options/user-themes").then((userThemes) => {
|
||||||
|
setThemes([
|
||||||
|
...BUILTIN_THEMES,
|
||||||
|
...userThemes
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsSection title={t("theme.layout")}>
|
<>
|
||||||
{!isMobile() && <FormRadioGroup
|
<OptionsSection title={t("theme.layout")}>
|
||||||
name="layout-orientation"
|
{!isMobile() && <FormRadioGroup
|
||||||
values={[
|
name="layout-orientation"
|
||||||
{
|
values={[
|
||||||
label: <><strong>{t("theme.layout-vertical-title")}</strong> - {t("theme.layout-vertical-description")}</>,
|
{
|
||||||
value: "vertical"
|
label: <><strong>{t("theme.layout-vertical-title")}</strong> - {t("theme.layout-vertical-description")}</>,
|
||||||
},
|
value: "vertical"
|
||||||
{
|
},
|
||||||
label: <><strong>{t("theme.layout-horizontal-title")}</strong> - {t("theme.layout-horizontal-description")}</>,
|
{
|
||||||
value: "horizontal"
|
label: <><strong>{t("theme.layout-horizontal-title")}</strong> - {t("theme.layout-horizontal-description")}</>,
|
||||||
}
|
value: "horizontal"
|
||||||
]}
|
}
|
||||||
currentValue={layoutOrientation} onChange={async (newValue) => {
|
]}
|
||||||
await setLayoutOrientation(newValue);
|
currentValue={layoutOrientation} onChange={setLayoutOrientation}
|
||||||
reloadFrontendApp("layout orientation change");
|
/>}
|
||||||
}}
|
</OptionsSection>
|
||||||
/>}
|
|
||||||
</OptionsSection>
|
<OptionsSection title={t("theme.title")}>
|
||||||
|
<Column md={6}>
|
||||||
|
<label>{t("theme.theme_label")}</label>
|
||||||
|
<FormSelect values={themes} currentValue={theme} onChange={setTheme} />
|
||||||
|
</Column>
|
||||||
|
</OptionsSection>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -6,14 +6,7 @@ import type { OptionMap } from "@triliumnext/commons";
|
|||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="options-section">
|
<div class="options-section">
|
||||||
<h4>${t("theme.title")}</h4>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="theme-select">${t("theme.theme_label")}</label>
|
|
||||||
<select id="theme-select" class="theme-select form-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 side-checkbox">
|
<div class="col-md-6 side-checkbox">
|
||||||
<label class="form-check tn-checkbox">
|
<label class="form-check tn-checkbox">
|
||||||
<input type="checkbox" class="override-theme-fonts form-check-input">
|
<input type="checkbox" class="override-theme-fonts form-check-input">
|
||||||
@@ -24,57 +17,20 @@ const TPL = /*html*/`
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Theme {
|
|
||||||
val: string;
|
|
||||||
title: string;
|
|
||||||
noteId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ThemeOptions extends OptionsWidget {
|
export default class ThemeOptions extends OptionsWidget {
|
||||||
|
|
||||||
private $themeSelect!: JQuery<HTMLElement>;
|
|
||||||
private $overrideThemeFonts!: JQuery<HTMLElement>;
|
private $overrideThemeFonts!: JQuery<HTMLElement>;
|
||||||
private $layoutOrientation!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.$themeSelect = this.$widget.find(".theme-select");
|
this.$themeSelect = this.$widget.find(".theme-select");
|
||||||
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
|
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
|
||||||
|
|
||||||
this.$themeSelect.on("change", async () => {
|
|
||||||
const newTheme = this.$themeSelect.val();
|
|
||||||
|
|
||||||
await server.put(`options/theme/${newTheme}`);
|
|
||||||
|
|
||||||
utils.reloadFrontendApp("theme change");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$overrideThemeFonts.on("change", () => this.updateCheckboxOption("overrideThemeFonts", this.$overrideThemeFonts));
|
this.$overrideThemeFonts.on("change", () => this.updateCheckboxOption("overrideThemeFonts", this.$overrideThemeFonts));
|
||||||
}
|
}
|
||||||
|
|
||||||
async optionsLoaded(options: OptionMap) {
|
async optionsLoaded(options: OptionMap) {
|
||||||
const themes: Theme[] = [
|
|
||||||
{ val: "next", title: t("theme.triliumnext") },
|
|
||||||
{ val: "next-light", title: t("theme.triliumnext-light") },
|
|
||||||
{ val: "next-dark", title: t("theme.triliumnext-dark") },
|
|
||||||
{ val: "auto", title: t("theme.auto_theme") },
|
|
||||||
{ val: "light", title: t("theme.light_theme") },
|
|
||||||
{ val: "dark", title: t("theme.dark_theme") }
|
|
||||||
].concat(await server.get<Theme[]>("options/user-themes"));
|
|
||||||
|
|
||||||
this.$themeSelect.empty();
|
|
||||||
|
|
||||||
for (const theme of themes) {
|
|
||||||
this.$themeSelect.append(
|
|
||||||
$("<option>")
|
|
||||||
.attr("value", theme.val)
|
|
||||||
.attr("data-note-id", theme.noteId || "")
|
|
||||||
.text(theme.title)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$themeSelect.val(options.theme);
|
|
||||||
|
|
||||||
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
|
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
|
||||||
|
|
||||||
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`).prop("checked", "true");
|
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`).prop("checked", "true");
|
||||||
|
|||||||
Reference in New Issue
Block a user