Skip to content

feat(dashboard) :主题切换支持跟随系统#8648

Open
lingyun14beta wants to merge 14 commits into
AstrBotDevs:masterfrom
lingyun14beta:feat(dashboard)-add-system-theme-mode-with-submenu-switcher
Open

feat(dashboard) :主题切换支持跟随系统#8648
lingyun14beta wants to merge 14 commits into
AstrBotDevs:masterfrom
lingyun14beta:feat(dashboard)-add-system-theme-mode-with-submenu-switcher

Conversation

@lingyun14beta
Copy link
Copy Markdown
Contributor

@lingyun14beta lingyun14beta commented Jun 7, 2026

原主题切换为单按钮二态切换(亮/暗),无法选择跟随系统。改为与语言切换菜单一致的子菜单交互,支持浅色、深色、跟随系统三个选项。

Modifications / 改动点

  • config.ts:新增 ThemeMode 类型和 themeMode 字段,初始化时自动迁移旧 uiTheme 数据
  • stores/customizer.ts:state 新增 themeMode,新增 SET_THEME_MODE action
  • main.ts:提取 setupThemeSync,注册全局唯一 matchMedia 监听器,system 模式下系统切换时自动更新主题
  • VerticalHeader.vue:主题入口改为 hover 展开子菜单
  • LoginPage.vue / SetupPage.vue:主题按钮改为点击展开下拉菜单
  • i18n(6 个文件):新增 systemtitle 翻译键(en-US / zh-CN / ru-RU)
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

image image

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Add a configurable theme mode with light, dark, and system options and centralize theme synchronization with system preferences.

New Features:

  • Introduce a themeMode setting supporting light, dark, and system-following modes across the dashboard, login, and setup pages.

Enhancements:

  • Refactor theme initialization to a single setupThemeSync helper that aligns Vuetify themes with stored preferences and system color scheme changes.
  • Update header and auth pages to use dropdown-style theme menus with localized labels instead of a binary toggle button.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:webui The bug / feature is about webui(dashboard) of astrbot. labels Jun 7, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The logic for resolving uiTheme from a ThemeMode (resolveUiThemeFromMode in stores/customizer.ts and resolveUiTheme in config.ts) is currently duplicated; consider centralizing this into a single shared helper to avoid divergence in future changes.
  • The theme-switch UI (options array, current icon computation, and dropdown markup) is implemented separately in VerticalHeader.vue, LoginPage.vue, and SetupPage.vue; extracting a reusable ThemeSwitcher component would reduce duplication and make future behavior changes easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The logic for resolving `uiTheme` from a `ThemeMode` (`resolveUiThemeFromMode` in `stores/customizer.ts` and `resolveUiTheme` in `config.ts`) is currently duplicated; consider centralizing this into a single shared helper to avoid divergence in future changes.
- The theme-switch UI (options array, current icon computation, and dropdown markup) is implemented separately in `VerticalHeader.vue`, `LoginPage.vue`, and `SetupPage.vue`; extracting a reusable `ThemeSwitcher` component would reduce duplication and make future behavior changes easier to maintain.

## Individual Comments

### Comment 1
<location path="dashboard/src/stores/customizer.ts" line_range="6-15" />
<code_context>
 const DARK_THEMES: ReadonlySet<string> = new Set(['PurpleThemeDark']);

-export const useCustomizerStore = defineStore("customizer", {
+function resolveUiThemeFromMode(mode: ThemeMode): string {
+  if (mode === 'dark') return 'PurpleThemeDark';
+  if (mode === 'light') return 'PurpleTheme';
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating theme-mode-to-uiTheme resolution logic between config and store.

`resolveUiTheme` in `config.ts` and `resolveUiThemeFromMode` here both map `ThemeMode``uiTheme`. Maintaining two mappings risks them diverging as themes evolve. Consider exporting a single helper (from `config.ts` or a shared theme util) and reusing it in the store so config and runtime logic share the same mapping.

Suggested implementation:

```typescript
import { defineStore } from 'pinia';
import config, { type ThemeMode, resolveUiTheme } from '@/config';

```

```typescript
const DARK_THEMES: ReadonlySet<string> = new Set(['PurpleThemeDark']);

const resolveUiThemeFromMode = resolveUiTheme;

```
</issue_to_address>

### Comment 2
<location path="dashboard/src/config.ts" line_range="1" />
<code_context>
+export type ThemeMode = 'light' | 'dark' | 'system';
+
 export type ConfigProps = {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the theme mode and uiTheme resolution (including localStorage and matchMedia handling) into a shared utility and letting this config file simply consume it.

You can reduce complexity and duplication by centralizing the `ThemeMode` + `uiTheme` logic in a small utility and keeping this config file “dumb” (just consuming that utility).

### 1. Extract theme mode utilities

Create a shared utility, e.g. `themeModeUtils.ts`:

```ts
// themeModeUtils.ts
export type ThemeMode = 'light' | 'dark' | 'system';

export function checkThemeMode(local: Storage): ThemeMode {
  const mode = local.getItem('themeMode') as ThemeMode | null;
  if (mode === 'light' || mode === 'dark' || mode === 'system') return mode;

  const legacyTheme = local.getItem('uiTheme');
  if (legacyTheme === 'PurpleThemeDark') {
    local.setItem('themeMode', 'dark');
    return 'dark';
  }
  if (legacyTheme === 'PurpleTheme') {
    local.setItem('themeMode', 'light');
    return 'light';
  }

  local.setItem('themeMode', 'system');
  return 'system';
}

export function resolveUiTheme(mode: ThemeMode, prefersDark: boolean): string {
  if (mode === 'dark') return 'PurpleThemeDark';
  if (mode === 'light') return 'PurpleTheme';
  return prefersDark ? 'PurpleThemeDark' : 'PurpleTheme';
}

export function resolveThemeState(local: Storage, win: Window | undefined) {
  const themeMode = checkThemeMode(local);
  const prefersDark =
    typeof win !== 'undefined' &&
    win.matchMedia('(prefers-color-scheme: dark)').matches;

  const uiTheme = resolveUiTheme(themeMode, prefersDark);
  local.setItem('uiTheme', uiTheme);

  return { themeMode, uiTheme };
}
```

This keeps all existing behavior (migration, `matchMedia`, `localStorage` side effects) in one place.

### 2. Simplify `config` to just consume the utility

Then your config file becomes simpler and has no duplicated logic:

```ts
// config.ts
import { ThemeMode, resolveThemeState } from './themeModeUtils';

export type ConfigProps = {
  Sidebar_drawer: boolean;
  Customizer_drawer: boolean;
  mini_sidebar: boolean;
  fontTheme: string;
  uiTheme: string;
  themeMode: ThemeMode;
  inputBg: boolean;
};

const { themeMode, uiTheme } = resolveThemeState(localStorage, window);

const config: ConfigProps = {
  Sidebar_drawer: true,
  Customizer_drawer: false,
  mini_sidebar: false,
  fontTheme: 'Roboto',
  uiTheme,
  themeMode,
  inputBg: false,
};

export default config;
```

### 3. Reuse the same utility in the store

In `stores/customizer.ts`, replace any `resolveUiThemeFromMode` / duplicated `matchMedia` logic with calls to `resolveThemeState` / `resolveUiTheme`. For example:

```ts
// stores/customizer.ts (sketch)
import { resolveUiTheme, ThemeMode } from '../themeModeUtils';

function someSetter(mode: ThemeMode) {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const uiTheme = resolveUiTheme(mode, prefersDark);
  // ...
}
```

This removes duplicated mapping and `matchMedia` code, reduces cognitive load, and guarantees config and store stay in sync while preserving all current features.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread dashboard/src/stores/customizer.ts Outdated
Comment thread dashboard/src/config.ts
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for a system theme mode ('light' | 'dark' | 'system') across the dashboard, updating the configuration, Pinia store, localization files, main entry point, and authentication pages. The review feedback focuses on enhancing environment safety by guarding localStorage and window.matchMedia calls for non-browser environments (like SSR or testing), reducing code duplication by reusing the resolveUiTheme helper in the store, and improving UI consistency by standardizing the system theme icon to mdi-theme-light-dark across all views.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread dashboard/src/config.ts Outdated
Comment thread dashboard/src/stores/customizer.ts Outdated
Comment thread dashboard/src/stores/customizer.ts Outdated
Comment thread dashboard/src/main.ts
Comment thread dashboard/src/main.ts
Comment thread dashboard/src/views/authentication/auth/LoginPage.vue
Comment thread dashboard/src/views/authentication/auth/LoginPage.vue
Comment thread dashboard/src/views/authentication/auth/SetupPage.vue
Comment thread dashboard/src/views/authentication/auth/SetupPage.vue
Comment thread dashboard/src/layouts/full/vertical-header/VerticalHeader.vue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant