feat(settings): per-theme visibility toggle (#2894)

Theme options gain an optional hidden flag in their JSON config. The
admin theme table gets a Visible checkbox per row; hidden themes are
filtered out of the user-facing theme picker and skipped by the
preferred-theme resolver so a hidden default or previously-selected
theme falls back cleanly.

Authored By: TDvorak <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 7f3463ec68
commit 649f361385

@ -841,7 +841,8 @@
"ssoRegisterEnabled": "Allow automatic registration",
"ssoRegisterEnabledDes": "Automatically create a local account when a user signs in via SSO for the first time. The sign-up email filter below also applies.",
"ssoCallbackUrl": "Callback URL",
"ssoCallbackUrlDes": "Register this URL as the redirect/callback URL in your identity provider: <0>{{url}}</0>"
"ssoCallbackUrlDes": "Register this URL as the redirect/callback URL in your identity provider: <0>{{url}}</0>",
"themeVisible": "Visible"
},
"giftCodes": {
"giftCodesSettings": "Gift Codes",

@ -841,7 +841,8 @@
"ssoRegisterEnabled": "允许自动注册",
"ssoRegisterEnabledDes": "用户首次通过 SSO 登录时自动创建本地账号。下方的注册邮箱过滤规则同样适用。",
"ssoCallbackUrl": "回调地址",
"ssoCallbackUrlDes": "请在身份提供方中将此 URL 注册为 Redirect/Callback URL:<0>{{url}}</0>"
"ssoCallbackUrlDes": "请在身份提供方中将此 URL 注册为 Redirect/Callback URL:<0>{{url}}</0>",
"themeVisible": "可见"
},
"giftCodes": {
"giftCodesSettings": "礼品码",

@ -242,6 +242,7 @@ interface themeOptions {
interface singleThemeOption {
light: ThemeOptions;
dark?: ThemeOptions;
hidden?: boolean;
}
const getPreferredTheme = (
@ -250,16 +251,21 @@ const getPreferredTheme = (
preferredTheme?: string,
defaultTheme?: string,
): ThemeOptions => {
// Hidden themes are not user-selectable; fall back to the default when the
// preferred (or the last remaining) theme is hidden.
const visible = Object.values(opts).filter((o) => !o.hidden);
const pool = visible.length > 0 ? visible : Object.values(opts);
let themeConfig = {} as singleThemeOption;
if (defaultTheme && opts[defaultTheme]) {
if (defaultTheme && opts[defaultTheme] && !opts[defaultTheme].hidden) {
themeConfig = opts[defaultTheme];
}
if (preferredTheme && opts[preferredTheme]) {
if (preferredTheme && opts[preferredTheme] && !opts[preferredTheme].hidden) {
themeConfig = opts[preferredTheme];
}
if (!themeConfig?.light) {
themeConfig = Object.values(opts)[0];
themeConfig = pool[0];
}
if (mode === "dark" && themeConfig.dark) {

@ -36,6 +36,7 @@ export interface ThemeOptionsProps {
interface ThemeOption {
id: string;
config: {
hidden?: boolean;
light: {
palette: {
primary: {
@ -234,6 +235,15 @@ const ThemeOptions = ({ value, onChange, defaultTheme, onDefaultThemeChange }: T
[onDefaultThemeChange],
);
const handleVisibilityChange = useCallback(
(id: string, visible: boolean) => {
const newOptions = { ...options };
newOptions[id] = { ...newOptions[id], hidden: !visible };
handleSave(newOptions);
},
[options, handleSave],
);
const optionsArray = useMemo(() => {
return Object.entries(options).map(([id, config]) => ({
id,
@ -256,6 +266,7 @@ const ThemeOptions = ({ value, onChange, defaultTheme, onDefaultThemeChange }: T
<TableHead>
<TableRow>
<NoWrapTableCell width={50}>{t("settings.defaultTheme")}</NoWrapTableCell>
<NoWrapTableCell width={50}>{t("settings.themeVisible")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("settings.primaryColor")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("settings.secondaryColor")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("settings.primaryColorDark")}</NoWrapTableCell>
@ -273,6 +284,13 @@ const ThemeOptions = ({ value, onChange, defaultTheme, onDefaultThemeChange }: T
onChange={() => handleDefaultThemeChange(option.id)}
/>
</TableCell>
<TableCell>
<StyledCheckbox
size="small"
checked={!option.config.hidden}
onChange={(e) => handleVisibilityChange(option.id, e.target.checked)}
/>
</TableCell>
<TableCell>
<HexColorInput
required

@ -233,7 +233,9 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => {
</SettingForm>
<SettingForm title={t("setting.themeColor")} lgWidth={12}>
<SelectorBox sx={{ gap: 1 }}>
{Object.keys(themeOptions).map((color, index) => (
{Object.keys(themeOptions)
.filter((color) => !themeOptions[color]?.hidden)
.map((color, index) => (
<ColorCircle
size={30}
color={color}

Loading…
Cancel
Save