Nuclear 主题商店实战指南:社区主题的浏览、安装、本地持久化与 Schema 校验
【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear
Nuclear 内置了一个主题商店(Theme Store),允许用户直接从官方主题注册表中浏览、安装并应用社区创作的主题。本篇以packages/docs/themes/theme-store.md文档为主线,逐节还原商店的浏览、安装、应用、卸载全流程,并结合 themeRegistryApi.ts、useInstallTheme.ts 等源码,讲清数据从哪里来、文件存到哪里去、以及"同一时间只能有一个主题生效"这条规则在状态层是如何实现的。
进入主题商店
从侧边栏打开 Preferences,进入 Themes 页面,切换到Store标签页即可进入主题商店。Store 标签页由 ThemeStore.tsx 实现,它有三种渲染状态:
- 拉取主题列表期间显示居中的加载器(
CenteredLoader); - 请求失败时显示错误提示文案(
store.loadError),不渲染列表; - 成功时渲染一个搜索输入框加主题条目列表,每个条目由 UI 包的
ThemeStoreItem组件呈现。
浏览:官方主题注册表与本地搜索
Store 标签页列出的主题来自官方主题注册表仓库NuclearPlayer/theme-registry。从源码看,ThemeRegistryApi 的构造函数把请求基址固定为该仓库经 jsDelivr CDN 分发的 master 分支,getThemes()方法拉取根目录下的themes.json文件,并用MarketplaceThemeRegistrySchema做整体校验后再返回registry.themes数组。
主题列表的获取走 React Query,见 useThemeRegistry.ts:
const STALE_TIME_MS = 5 * 60 * 1000; export const useMarketplaceThemes = () => { return useQuery<MarketplaceTheme[]>({ queryKey: MARKETPLACE_THEMES_QUERY_KEY, queryFn: () => themeRegistryApi.getThemes(), staleTime: STALE_TIME_MS, }); };staleTime设为 5 分钟,意味着页面内切换标签不会立刻重复请求,注册表数据在半小时内基本只拉取一次。
每个主题条目展示的信息包括:色彩调色板预览(对角排布的色块,即四元组palette)、名称、描述、作者和标签。搜索栏是纯前端过滤,实现在 useFilteredMarketplaceThemes.ts:
const matchesSearch = (theme: MarketplaceTheme, query: string): boolean => { const lower = query.toLowerCase(); return ( theme.name.toLowerCase().includes(lower) || theme.description.toLowerCase().includes(lower) || theme.author.toLowerCase().includes(lower) || (theme.tags?.some((tag) => tag.toLowerCase().includes(lower)) ?? false) ); };可以确认搜索是大小写不敏感、按子串匹配,覆盖 name、description、author、tags 四个字段——与官方文档描述的过滤范围完全一致。
安装主题:下载到本地themes/store/目录
在任意主题上点击Install,Nuclear 会从注册表下载主题文件并保存到本地;下载期间按钮显示进度态,完成后切换为Installed。这条流程的核心逻辑在 useInstallTheme.ts,它是一个 React Query mutation:
mutationFn: async ({ theme }: InstallThemeParams) => { const themeFile = await themeRegistryApi.getThemeFile(theme.path); await ensureDir('themes/store'); await writeTextFile( `themes/store/${theme.id}.json`, JSON.stringify(themeFile, null, 2), { baseDir: BaseDirectory.AppData }, ); return theme; },可以从中读出三个关键事实:
- 下载前经过 Schema 校验。
getThemeFile(path)调用themeRegistryApi.getThemeFile(),该文件会用AdvancedThemeSchema解析(见 themeRegistryApi.ts),即落盘的一定是一个结构合法的进阶主题 JSON,坏文件不会进入本地目录。 - 落盘路径固定。先用
ensureDir('themes/store')确保子目录存在,再写入themes/store/<theme.id>.json,基目录是 Tauri 的BaseDirectory.AppData(应用数据目录),写入时以 2 空格缩进格式化,方便用户手工查看。 - 安装成功后立即更新内存状态。
onSuccess回调把{ id, name, path: 'themes/store/<id>.json' }追加进useThemeStore的marketplaceThemes列表,并主动失效marketplace-themes查询缓存。
出错时走onError,通过 reportError 弹出形如"安装主题 X 失败"的本地化提示(store.installError)。
安装完成后,该主题会出现在My Themes标签页下的Store themes下拉框中。这个下拉框由 MarketplaceThemeSelect.tsx 渲染,它有一个值得注意的细节:
if (marketplaceThemes.length === 0) { return null; }即只要还没有安装任何商店主题,My Themes 页面里的Store themes区块整体不渲染(见 MyThemes.tsx),与文档"My Themes 下出现 Store themes 下拉框"的描述吻合。
应用主题:双入口与"唯一生效"规则
已安装的商店主题有两种应用方式:
- 在My Themes标签页的Store themes下拉框中选择;
- 在Store标签页的主题条目上直接点Apply。
两个入口最终汇聚到同一个函数loadAndApplyMarketplaceTheme(advancedThemeService.ts):
export const loadAndApplyMarketplaceTheme = async (id: string) => { const path = useThemeStore.getState().getMarketplaceThemePath(id); if (!path) return; await loadAndApplyThemeFile(path); await useThemeStore.getState().selectMarketplaceTheme(id); };它先根据主题 id 从marketplaceThemes中反查本地路径,读文件、parseAdvancedTheme校验、setThemeId('')清掉内置基础主题的 id,再applyAdvancedTheme(theme)把 CSS 变量应用到全局,最后调用selectMarketplaceTheme(id)记录选中状态。
"选择商店主题会取消任何基础主题或进阶主题,同一时间只有一个主题生效"这条规则,根源在 themeStore.ts 的类型设计:
export type BasicTheme = { type: 'basic'; id: string }; export type AdvancedTheme = { type: 'advanced'; path: string }; export type MarketplaceTheme = { type: 'marketplace'; id: string }; export type ActiveTheme = BasicTheme | AdvancedTheme | MarketplaceTheme;activeTheme是一个三选一的可辨识联合类型,状态槽只能容纳一种主题,因此选中任一类型天然排除了其余两者。selectBasicTheme/selectAdvancedTheme/selectMarketplaceTheme三个 action 都只是替换activeTheme并通过persistActiveTheme把core.theme.active.type与core.theme.active.id两个设置写入设置存储(见 themeStore.ts),所以选择会跨启动保留;应用启动时hydrate()读回这两个设置恢复状态,applyThemeFromSettingsIfAny()再按类型解析出文件路径重新应用(商店主题走getMarketplaceThemePath分支)。
Store 标签页中,当前生效的商店主题条目会显示对勾(isActive属性),其判定逻辑是isMarketplaceThemeActive(id):activeTheme.type === 'marketplace' && activeTheme.id === id。
卸载主题
在 Store 标签页对已安装主题点击垃圾桶图标即可卸载。底层实现在 useUninstallTheme.ts:
mutationFn: async ({ path }: UninstallThemeParams) => { await remove(path, { baseDir: BaseDirectory.AppData }); }, onSuccess: (_, { id }) => { const store = useThemeStore.getState(); store.setMarketplaceThemes( store.marketplaceThemes.filter((theme) => theme.id !== id), ); if (store.isMarketplaceThemeActive(id)) { clearAdvancedTheme(); store.selectBasicTheme(DEFAULT_THEME_ID); } },注意被删除的路径正是 ThemeStore.tsx 传入的themes/store/${theme.id}.json,与安装时的落盘路径一一对应。卸载成功后:主题从内存列表中移除;如果该主题当前正在生效,则清空高级主题并回退到默认基础主题(DEFAULT_THEME_ID)——这就是文档所述"若该主题是激活状态,Nuclear 重置为默认主题"的具体实现。失败时同样以store.uninstallError提示。
商店主题的本地存储位置
商店主题存放在应用数据目录下的themes/store/子目录中,与用户自建进阶主题所在的themes/目录分离(后者见 进阶主题文档)。各平台具体位置:
- Linux:
~/.local/share/com.nuclearplayer/themes/store/ - macOS:
~/Library/Application Support/com.nuclearplayer/themes/store/ - Windows:
%APPDATA%/com.nuclearplayer/themes/store/
源码中统一以BaseDirectory.AppData为基目录 + 相对路径themes/store/<id>.json表达(见 useInstallTheme.ts 与 useUninstallTheme.ts),Tauri 按平台将其解析为上表三个位置。由于每个商店主题就是一个独立、可读的 JSON 文件,你可以直接打开它查看颜色变量,也可以自行修改后重新应用。
为商店创建主题:进阶主题 + 额外元数据
商店主题是标准的进阶主题 JSON 文件,附加了若干元数据字段:author、description、tags以及用于预览的palette。提交所需的完整格式与流程以官方主题注册表仓库的说明为准。
客户端的校验规则能精确告诉我们这些字段各自是否必填,见 schema.ts:
export const AdvancedThemeSchema = z.object({ version: ThemeVersion, // 必须是 1 name: z.string().min(1), author: z.string().min(1).optional(), description: z.string().optional(), tags: z.array(z.string()).optional(), palette: z.tuple([z.string(), z.string(), z.string(), z.string()]).optional(), vars: ThemeVars.optional(), dark: ThemeVars.optional(), }); export const MarketplaceThemeSchema = AdvancedThemeSchema.pick({ name: true, author: true, description: true, tags: true, palette: true, }) .required({ author: true, description: true, palette: true }) .extend({ id: z.string().min(1), path: z.string().min(1), });从源码结构看可以总结出商店主题的字段要求:
| 字段 | 本地进阶主题 | 商店注册表条目 | 说明 |
|---|---|---|---|
version | 必填,固定为1 | —(由注册表整体 schema 管理版本) | 见 themes-advanced.md |
name | 必填 | 必填 | 主题显示名 |
author | 可选 | 必填 | 商店条目必须署名 |
description | 可选 | 必填 | 商店条目必须有描述 |
tags | 可选 | 可选(但参与搜索过滤) | 字符串数组 |
palette | 可选 | 必填 | 固定 4 个颜色值,用于商店里对角色块预览 |
id/path | — | 必填 | 注册表条目标识与文件相对路径 |
vars/dark | 可选 | 下载时按AdvancedThemeSchema校验 | 颜色、字体、边框、圆角、阴影变量,变量名不带--前缀 |
两点补充:其一,用户安装时下载并落盘的是完整主题文件(含vars/dark),MarketplaceThemeSchema只用于校验注册表themes.json里的元数据条目,两者分工不同;其二,palette必须是恰好四个字符串的元组,这对应界面预览中固定展示的四个对角色块。
关键源码索引
| 文件 | 职责 |
|---|---|
| packages/player/src/apis/themeRegistryApi.ts | 注册表请求与 Schema 校验 |
| packages/player/src/hooks/useThemeRegistry.ts | 主题列表查询(5 分钟 staleTime) |
| packages/player/src/hooks/useFilteredMarketplaceThemes.ts | 搜索过滤(name/description/author/tags) |
| packages/player/src/hooks/useInstallTheme.ts | 安装:下载、ensureDir、写入themes/store/ |
| packages/player/src/hooks/useUninstallTheme.ts | 卸载:删文件、状态回退默认主题 |
| packages/player/src/views/Themes/ThemeStore.tsx | Store 标签页 UI |
| packages/player/src/views/Themes/MarketplaceThemeSelect.tsx | My Themes 下的 Store themes 下拉框 |
| packages/player/src/services/advancedThemeService.ts | 加载并应用商店主题、启动时恢复 |
| packages/player/src/stores/themeStore.ts | 唯一生效主题的三态联合类型与持久化 |
| packages/themes/src/advanced/schema.ts | 进阶主题与商店条目 Zod Schema |
【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考