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", "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.", "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", "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": { "giftCodes": {
"giftCodesSettings": "Gift Codes", "giftCodesSettings": "Gift Codes",
@ -1783,4 +1784,4 @@
"shareID": "Shared ID", "shareID": "Shared ID",
"reason": "Reason" "reason": "Reason"
} }
} }

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

@ -242,6 +242,7 @@ interface themeOptions {
interface singleThemeOption { interface singleThemeOption {
light: ThemeOptions; light: ThemeOptions;
dark?: ThemeOptions; dark?: ThemeOptions;
hidden?: boolean;
} }
const getPreferredTheme = ( const getPreferredTheme = (
@ -250,16 +251,21 @@ const getPreferredTheme = (
preferredTheme?: string, preferredTheme?: string,
defaultTheme?: string, defaultTheme?: string,
): ThemeOptions => { ): 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; let themeConfig = {} as singleThemeOption;
if (defaultTheme && opts[defaultTheme]) { if (defaultTheme && opts[defaultTheme] && !opts[defaultTheme].hidden) {
themeConfig = opts[defaultTheme]; themeConfig = opts[defaultTheme];
} }
if (preferredTheme && opts[preferredTheme]) { if (preferredTheme && opts[preferredTheme] && !opts[preferredTheme].hidden) {
themeConfig = opts[preferredTheme]; themeConfig = opts[preferredTheme];
} }
if (!themeConfig?.light) { if (!themeConfig?.light) {
themeConfig = Object.values(opts)[0]; themeConfig = pool[0];
} }
if (mode === "dark" && themeConfig.dark) { if (mode === "dark" && themeConfig.dark) {

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

@ -233,14 +233,16 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => {
</SettingForm> </SettingForm>
<SettingForm title={t("setting.themeColor")} lgWidth={12}> <SettingForm title={t("setting.themeColor")} lgWidth={12}>
<SelectorBox sx={{ gap: 1 }}> <SelectorBox sx={{ gap: 1 }}>
{Object.keys(themeOptions).map((color, index) => ( {Object.keys(themeOptions)
<ColorCircle .filter((color) => !themeOptions[color]?.hidden)
size={30} .map((color, index) => (
color={color} <ColorCircle
onClick={() => applyTheme(color)} size={30}
selected={(preferredTheme && preferredTheme == color) || (!preferredTheme && defaultTheme == color)} color={color}
/> onClick={() => applyTheme(color)}
))} selected={(preferredTheme && preferredTheme == color) || (!preferredTheme && defaultTheme == color)}
/>
))}
</SelectorBox> </SelectorBox>
</SettingForm> </SettingForm>
<SettingForm title={t("setting.versionRetention")}> <SettingForm title={t("setting.versionRetention")}>

Loading…
Cancel
Save