mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
remove iro file association, show theme descriptions
This commit is contained in:
@@ -40,7 +40,7 @@ Save your entire layout — scope arrangement, visibility, sizes, per-scope sett
|
||||
|
||||
## Themes
|
||||
|
||||
The whole interface is themeable through `.iro` files. Themes control everything — spectrum heatmap gradients, oscilloscope trace color, vectorscope bands, all of it. Build your own or import someone else's.
|
||||
The whole interface is themeable through editable `.iro` files. Themes control everything — spectrum heatmap gradients, oscilloscope trace color, vectorscope bands, all of it. Build your own in an editor, drop it into the `Prism Themes` folder, or import it from Prism.
|
||||
|
||||
<!--  -->
|
||||
|
||||
|
||||
@@ -72,12 +72,6 @@
|
||||
"name": "Prism Profile",
|
||||
"description": "Prism shareable profile",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "iro",
|
||||
"name": "Prism Theme",
|
||||
"description": "Prism shareable theme",
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
|
||||
@@ -59,7 +59,6 @@ let nowPlayingConfigBoundsTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const windowSettingsHeights = new Map<number, number>()
|
||||
const windowSettingsBottomAnchors = new Map<number, number>()
|
||||
const pendingProfileOpenPaths: string[] = []
|
||||
const pendingThemeOpenPaths: string[] = []
|
||||
|
||||
let profileLibrary: FileBackedProfileLibrary | null = null
|
||||
let themeLibrary: FileBackedThemeLibrary | null = null
|
||||
@@ -162,13 +161,6 @@ function getNowPlayingManager(): NowPlayingManager {
|
||||
return nowPlayingManager
|
||||
}
|
||||
|
||||
function broadcastThemeSnapshot(snapshot: ThemeLibrarySnapshot): void {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (window.isDestroyed() || window.webContents.isDestroyed()) continue
|
||||
window.webContents.send('themes:external-activated', snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
function queueProfileOpenPath(filePath: string): void {
|
||||
if (extname(filePath).toLowerCase() !== '.prsm') return
|
||||
|
||||
@@ -184,33 +176,12 @@ function queueProfileOpenPaths(paths: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function queueThemeOpenPath(filePath: string): void {
|
||||
if (extname(filePath).toLowerCase() !== '.iro') return
|
||||
|
||||
const resolvedPath = resolve(filePath)
|
||||
if (!pendingThemeOpenPaths.includes(resolvedPath)) {
|
||||
pendingThemeOpenPaths.push(resolvedPath)
|
||||
}
|
||||
}
|
||||
|
||||
function queueThemeOpenPaths(paths: string[]): void {
|
||||
for (const filePath of paths) {
|
||||
queueThemeOpenPath(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
function extractProfilePathsFromArgv(argv: string[]): string[] {
|
||||
return argv
|
||||
.filter((value) => extname(value).toLowerCase() === '.prsm')
|
||||
.map((value) => resolve(value))
|
||||
}
|
||||
|
||||
function extractThemePathsFromArgv(argv: string[]): string[] {
|
||||
return argv
|
||||
.filter((value) => extname(value).toLowerCase() === '.iro')
|
||||
.map((value) => resolve(value))
|
||||
}
|
||||
|
||||
function focusMainWindow(): void {
|
||||
if (!mainWindow) return
|
||||
if (mainWindow.isMinimized()) {
|
||||
@@ -221,12 +192,6 @@ function focusMainWindow(): void {
|
||||
raiseMainWindowAboveNormalPopouts()
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
return error instanceof Error && error.message
|
||||
? error.message
|
||||
: fallback
|
||||
}
|
||||
|
||||
function normalizeExternalHttpUrl(raw: string): string | null {
|
||||
if (typeof raw !== 'string' || !raw.trim()) {
|
||||
return null
|
||||
@@ -257,32 +222,6 @@ async function processPendingProfileOpenPaths(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function processPendingThemeOpenPaths(): Promise<void> {
|
||||
if (pendingThemeOpenPaths.length === 0) return
|
||||
|
||||
const paths = [...pendingThemeOpenPaths]
|
||||
pendingThemeOpenPaths.length = 0
|
||||
|
||||
let latestSnapshot: ThemeLibrarySnapshot | null = null
|
||||
|
||||
for (const filePath of paths) {
|
||||
try {
|
||||
latestSnapshot = await getThemeLibrary().importThemeFromPath(filePath)
|
||||
} catch (error) {
|
||||
dialog.showErrorBox(
|
||||
'Could Not Open Theme',
|
||||
getErrorMessage(error, `Prism could not open ${filePath}.`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!latestSnapshot || !mainWindow || mainWindow.isDestroyed()) return
|
||||
|
||||
applyNativeThemeSnapshot(latestSnapshot)
|
||||
focusMainWindow()
|
||||
broadcastThemeSnapshot(latestSnapshot)
|
||||
}
|
||||
|
||||
function applyNativeThemeSnapshot(snapshot: ThemeLibrarySnapshot): void {
|
||||
const activeTheme = snapshot.activeThemeId
|
||||
? snapshot.themes[snapshot.activeThemeId] ?? null
|
||||
@@ -1386,28 +1325,24 @@ function setupIPC(): void {
|
||||
ipcMain.handle('themes:load', async (_event, id: string) => {
|
||||
const snapshot = await getThemeLibrary().loadTheme(id)
|
||||
applyNativeThemeSnapshot(snapshot)
|
||||
broadcastThemeSnapshot(snapshot)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
ipcMain.handle('themes:rename', async (_event, id: string, name: string) => {
|
||||
const snapshot = await getThemeLibrary().renameTheme(id, name)
|
||||
applyNativeThemeSnapshot(snapshot)
|
||||
broadcastThemeSnapshot(snapshot)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
ipcMain.handle('themes:delete', async (_event, id: string) => {
|
||||
const snapshot = await getThemeLibrary().deleteTheme(id)
|
||||
applyNativeThemeSnapshot(snapshot)
|
||||
broadcastThemeSnapshot(snapshot)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
ipcMain.handle('themes:reload', async () => {
|
||||
const snapshot = await getThemeLibrary().reloadThemes()
|
||||
applyNativeThemeSnapshot(snapshot)
|
||||
broadcastThemeSnapshot(snapshot)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
@@ -1432,7 +1367,6 @@ function setupIPC(): void {
|
||||
|
||||
const snapshot = await getThemeLibrary().importThemeFromPath(result.filePaths[0])
|
||||
applyNativeThemeSnapshot(snapshot)
|
||||
broadcastThemeSnapshot(snapshot)
|
||||
return snapshot
|
||||
})
|
||||
|
||||
@@ -1447,7 +1381,6 @@ function setupIPC(): void {
|
||||
ipcMain.handle('themes:migrate-legacy', async (_event, payload: LegacyThemeMigrationPayload) => {
|
||||
const migration = await getThemeLibrary().migrateLegacyTheme(payload)
|
||||
applyNativeThemeSnapshot(migration.snapshot)
|
||||
broadcastThemeSnapshot(migration.snapshot)
|
||||
return migration
|
||||
})
|
||||
|
||||
@@ -1614,28 +1547,22 @@ if (!hasSingleInstanceLock) {
|
||||
await syncNativeThemeAppearance()
|
||||
createMainWindow()
|
||||
queueProfileOpenPaths(extractProfilePathsFromArgv(process.argv))
|
||||
queueThemeOpenPaths(extractThemePathsFromArgv(process.argv))
|
||||
void processPendingProfileOpenPaths()
|
||||
void processPendingThemeOpenPaths()
|
||||
})
|
||||
|
||||
app.on('open-file', (event, filePath) => {
|
||||
event.preventDefault()
|
||||
queueProfileOpenPath(filePath)
|
||||
queueThemeOpenPath(filePath)
|
||||
if (app.isReady()) {
|
||||
void processPendingProfileOpenPaths()
|
||||
void processPendingThemeOpenPaths()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
queueProfileOpenPaths(extractProfilePathsFromArgv(argv))
|
||||
queueThemeOpenPaths(extractThemePathsFromArgv(argv))
|
||||
if (app.isReady()) {
|
||||
focusMainWindow()
|
||||
void processPendingProfileOpenPaths()
|
||||
void processPendingThemeOpenPaths()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,11 +169,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.on('profiles:external-activated', handler)
|
||||
return () => ipcRenderer.removeListener('profiles:external-activated', handler)
|
||||
},
|
||||
onExternalThemeActivated: (callback: (snapshot: ThemeLibrarySnapshot) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, snapshot: ThemeLibrarySnapshot): void => callback(snapshot)
|
||||
ipcRenderer.on('themes:external-activated', handler)
|
||||
return () => ipcRenderer.removeListener('themes:external-activated', handler)
|
||||
},
|
||||
onScopePopoutReady: (callback: (kind: ScopeKind) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, kind: ScopeKind): void => callback(kind)
|
||||
ipcRenderer.on('scope-popout:ready', handler)
|
||||
|
||||
@@ -27,7 +27,6 @@ export default function App(): JSX.Element {
|
||||
const showProfilesFolder = useSettingsStore((s) => s.showProfilesFolder)
|
||||
const updateMainWindowBounds = useSettingsStore((s) => s.updateMainWindowBounds)
|
||||
const initializeThemes = useThemeStore((s) => s.initializeThemes)
|
||||
const applyExternalThemeSnapshot = useThemeStore((s) => s.applyExternalThemeSnapshot)
|
||||
const initializeNowPlaying = useNowPlayingStore((s) => s.initialize)
|
||||
const setNowPlayingConsumerActive = useNowPlayingStore((s) => s.setConsumerActive)
|
||||
const scopeOrder = useSettingsStore((s) => s.scopeOrder)
|
||||
@@ -64,9 +63,6 @@ export default function App(): JSX.Element {
|
||||
const unsubscribeProfile = window.electronAPI.onExternalProfileActivated((snapshot) => {
|
||||
applyExternalProfileSnapshot(snapshot)
|
||||
})
|
||||
const unsubscribeTheme = window.electronAPI.onExternalThemeActivated((snapshot) => {
|
||||
applyExternalThemeSnapshot(snapshot)
|
||||
})
|
||||
const unsubscribeBounds = window.electronAPI.onMainWindowBoundsChanged((bounds) => {
|
||||
updateMainWindowBounds(bounds)
|
||||
})
|
||||
@@ -124,14 +120,12 @@ export default function App(): JSX.Element {
|
||||
return () => {
|
||||
isDisposed = true
|
||||
unsubscribeProfile()
|
||||
unsubscribeTheme()
|
||||
unsubscribeBounds()
|
||||
unsubscribeExternalOpenRequested()
|
||||
unsubscribeCloseRequested()
|
||||
}
|
||||
}, [
|
||||
applyExternalProfileSnapshot,
|
||||
applyExternalThemeSnapshot,
|
||||
guardProfileTransition,
|
||||
importProfileFromPath,
|
||||
initializeProfiles,
|
||||
|
||||
@@ -47,38 +47,43 @@ function getErrorMessage(error: unknown, fallback: string): string {
|
||||
type ThemeCreditSource = {
|
||||
credit?: string
|
||||
website?: string
|
||||
description?: string
|
||||
} | null | undefined
|
||||
|
||||
export function resolveThemeCreditDetails(theme: ThemeCreditSource): {
|
||||
credit: string | null
|
||||
url: string | null
|
||||
description: string | null
|
||||
} {
|
||||
const credit = typeof theme?.credit === 'string' && theme.credit.trim()
|
||||
? theme.credit.trim()
|
||||
: null
|
||||
|
||||
if (!credit) {
|
||||
return { credit: null, url: null }
|
||||
return { credit: null, url: null, description: null }
|
||||
}
|
||||
|
||||
const website = typeof theme?.website === 'string' && theme.website.trim()
|
||||
? theme.website.trim()
|
||||
: null
|
||||
const description = typeof theme?.description === 'string' && theme.description.trim()
|
||||
? theme.description.trim()
|
||||
: null
|
||||
|
||||
if (!website) {
|
||||
return { credit, url: null }
|
||||
return { credit, url: null, description }
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(website)
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||
return { credit, url: parsed.toString() }
|
||||
return { credit, url: parsed.toString(), description }
|
||||
}
|
||||
} catch {
|
||||
// Invalid URLs fall back to plain credit text.
|
||||
}
|
||||
|
||||
return { credit, url: null }
|
||||
return { credit, url: null, description }
|
||||
}
|
||||
|
||||
export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element {
|
||||
@@ -368,20 +373,28 @@ export default function BottomBar({ onClose, onHeightChange }: BottomBarProps):
|
||||
<div className="bottom-bar__section-header">
|
||||
<div className="bottom-bar__section-title">Theme</div>
|
||||
{themeCredit.credit ? (
|
||||
themeCredit.url ? (
|
||||
<a
|
||||
className="bottom-bar__theme-credit bottom-bar__theme-credit--link"
|
||||
href={themeCredit.url}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
void handleOpenThemeWebsite(themeCredit.url!)
|
||||
}}
|
||||
>
|
||||
By {themeCredit.credit}
|
||||
</a>
|
||||
) : (
|
||||
<span className="bottom-bar__theme-credit">By {themeCredit.credit}</span>
|
||||
)
|
||||
<span className="bottom-bar__theme-metadata">
|
||||
{themeCredit.url ? (
|
||||
<a
|
||||
className="bottom-bar__theme-credit bottom-bar__theme-credit--link"
|
||||
href={themeCredit.url}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
void handleOpenThemeWebsite(themeCredit.url!)
|
||||
}}
|
||||
>
|
||||
By {themeCredit.credit}
|
||||
</a>
|
||||
) : (
|
||||
<span className="bottom-bar__theme-credit">By {themeCredit.credit}</span>
|
||||
)}
|
||||
{themeCredit.description ? (
|
||||
<span className="bottom-bar__theme-description">
|
||||
<span className="bottom-bar__theme-separator" aria-hidden="true">·</span>
|
||||
<span>{themeCredit.description}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="bottom-bar__section-body">
|
||||
|
||||
@@ -217,7 +217,6 @@ function ProviderIcon({ providerId }: { providerId: NowPlayingProviderId }): JSX
|
||||
|
||||
export default function NowPlayingConfigWindow(): JSX.Element {
|
||||
const initializeThemes = useThemeStore((s) => s.initializeThemes)
|
||||
const applyExternalThemeSnapshot = useThemeStore((s) => s.applyExternalThemeSnapshot)
|
||||
const initializeNowPlaying = useNowPlayingStore((s) => s.initialize)
|
||||
const nowPlayingState = useNowPlayingStore((s) => s.nowPlayingState)
|
||||
const saveProviderConfig = useNowPlayingStore((s) => s.saveProviderConfig)
|
||||
@@ -245,17 +244,11 @@ export default function NowPlayingConfigWindow(): JSX.Element {
|
||||
})
|
||||
})
|
||||
|
||||
const unsubscribeTheme = window.electronAPI.onExternalThemeActivated((snapshot) => {
|
||||
applyExternalThemeSnapshot(snapshot)
|
||||
})
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
unsubscribeTheme()
|
||||
window.electronAPI.stopWindowMove()
|
||||
}
|
||||
}, [
|
||||
applyExternalThemeSnapshot,
|
||||
initializeNowPlaying,
|
||||
initializeThemes,
|
||||
showBanner,
|
||||
|
||||
Vendored
-1
@@ -105,7 +105,6 @@ declare global {
|
||||
onProfileMenuShowFolder: (callback: () => void) => () => void
|
||||
onExternalProfileOpenRequested: (callback: (path: string) => void) => () => void
|
||||
onExternalProfileActivated: (callback: (snapshot: ProfileLibrarySnapshot) => void) => () => void
|
||||
onExternalThemeActivated: (callback: (snapshot: ThemeLibrarySnapshot) => void) => () => void
|
||||
onScopePopoutReady: (callback: (kind: ScopeKind) => void) => () => void
|
||||
onScopePopoutCloseRequested: (callback: (kind: ScopeKind) => void) => () => void
|
||||
onScopePopoutBoundsChanged: (callback: (kind: ScopeKind, bounds: WindowBounds) => void) => () => void
|
||||
|
||||
@@ -20,7 +20,6 @@ interface ThemeState {
|
||||
activeTheme: PrismResolvedTheme
|
||||
accent: string
|
||||
initializeThemes: () => Promise<void>
|
||||
applyExternalThemeSnapshot: (snapshot: ThemeLibrarySnapshot) => void
|
||||
loadTheme: (id: string) => Promise<void>
|
||||
renameTheme: (id: string, name: string) => Promise<void>
|
||||
deleteTheme: (id: string) => Promise<void>
|
||||
@@ -104,10 +103,6 @@ export const useThemeStore = create<ThemeState>((set) => ({
|
||||
applyThemeSnapshot(set, snapshot)
|
||||
},
|
||||
|
||||
applyExternalThemeSnapshot: (snapshot) => {
|
||||
applyThemeSnapshot(set, snapshot)
|
||||
},
|
||||
|
||||
loadTheme: async (id: string) => {
|
||||
if (!canUseElectronAPI()) return
|
||||
const snapshot = await window.electronAPI.loadTheme(id)
|
||||
|
||||
@@ -1604,7 +1604,9 @@ select {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bottom-bar__theme-credit {
|
||||
.bottom-bar__theme-metadata {
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
@@ -1614,6 +1616,18 @@ select {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bottom-bar__theme-credit {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.bottom-bar__theme-description {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.bottom-bar__theme-separator {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.bottom-bar__theme-credit--link {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
|
||||
@@ -2113,33 +2113,53 @@ test('resolveNativeThemeSource follows the active theme brightness for native UI
|
||||
assert.equal(resolveNativeThemeSource(lightTheme), 'light')
|
||||
})
|
||||
|
||||
test('resolveThemeCreditDetails enables links only for valid http and https theme websites', () => {
|
||||
test('resolveThemeCreditDetails normalizes descriptions and enables links only for valid http and https theme websites', () => {
|
||||
assert.deepEqual(
|
||||
resolveThemeCreditDetails({ credit: 'Night Shift', website: 'https://themes.example/night' }),
|
||||
resolveThemeCreditDetails({
|
||||
credit: 'Night Shift',
|
||||
website: 'https://themes.example/night',
|
||||
description: ' Soft neon palette ',
|
||||
}),
|
||||
{
|
||||
credit: 'Night Shift',
|
||||
url: 'https://themes.example/night',
|
||||
description: 'Soft neon palette',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
resolveThemeCreditDetails({ credit: 'Night Shift', website: 'http://themes.example/night' }),
|
||||
resolveThemeCreditDetails({
|
||||
credit: 'Night Shift',
|
||||
website: 'http://themes.example/night',
|
||||
description: 'Soft neon palette',
|
||||
}),
|
||||
{
|
||||
credit: 'Night Shift',
|
||||
url: 'http://themes.example/night',
|
||||
description: 'Soft neon palette',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
resolveThemeCreditDetails({ credit: 'Night Shift', website: 'ftp://themes.example/night' }),
|
||||
resolveThemeCreditDetails({
|
||||
credit: 'Night Shift',
|
||||
website: 'ftp://themes.example/night',
|
||||
description: 'Soft neon palette',
|
||||
}),
|
||||
{
|
||||
credit: 'Night Shift',
|
||||
url: null,
|
||||
description: 'Soft neon palette',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
resolveThemeCreditDetails({ credit: 'Night Shift', website: 'not a url' }),
|
||||
resolveThemeCreditDetails({
|
||||
credit: 'Night Shift',
|
||||
website: 'not a url',
|
||||
description: 'Soft neon palette',
|
||||
}),
|
||||
{
|
||||
credit: 'Night Shift',
|
||||
url: null,
|
||||
description: 'Soft neon palette',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
@@ -2147,6 +2167,15 @@ test('resolveThemeCreditDetails enables links only for valid http and https them
|
||||
{
|
||||
credit: null,
|
||||
url: null,
|
||||
description: null,
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
resolveThemeCreditDetails({ description: 'Soft neon palette' }),
|
||||
{
|
||||
credit: null,
|
||||
url: null,
|
||||
description: null,
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -2157,11 +2186,17 @@ test('BottomBar theme section renders compact credit metadata and opens valid li
|
||||
|
||||
assert.match(componentSource, /const themeCredit = resolveThemeCreditDetails\(activeTheme\)/)
|
||||
assert.match(componentSource, /window\.electronAPI\.openExternalUrl\(url\)/)
|
||||
assert.match(componentSource, /bottom-bar__theme-metadata/)
|
||||
assert.match(componentSource, /By \{themeCredit\.credit\}/)
|
||||
assert.match(componentSource, /themeCredit\.description \?/)
|
||||
assert.match(componentSource, /bottom-bar__theme-description/)
|
||||
assert.match(componentSource, /bottom-bar__theme-separator/)
|
||||
assert.match(componentSource, /bottom-bar__section-header/)
|
||||
assert.match(componentSource, /bottom-bar__theme-credit--link/)
|
||||
assert.match(stylesSource, /\.bottom-bar__section--theme \{[\s\S]*min-width: 420px;/)
|
||||
assert.match(stylesSource, /\.bottom-bar__section-header \{/)
|
||||
assert.match(stylesSource, /\.bottom-bar__theme-metadata \{/)
|
||||
assert.match(stylesSource, /\.bottom-bar__theme-description \{/)
|
||||
assert.match(stylesSource, /\.bottom-bar__theme-credit--link \{/)
|
||||
})
|
||||
|
||||
|
||||
@@ -69,18 +69,21 @@ test('theme files round-trip and keep grouped sections intact', () => {
|
||||
assert.equal(parsed.nowPlaying.background, theme.nowPlaying.background)
|
||||
})
|
||||
|
||||
test('theme files preserve optional credit and website metadata', () => {
|
||||
test('theme files preserve optional credit, website, and description metadata', () => {
|
||||
const theme = createDefaultTheme()
|
||||
theme.credit = 'Night Shift'
|
||||
theme.website = 'https://themes.example/night-shift'
|
||||
theme.description = 'Soft neon palette for late sessions'
|
||||
|
||||
const serialized = serializeThemeFile(theme)
|
||||
const parsed = parseThemeFileContent(serialized, DEFAULT_THEME_NAME)
|
||||
|
||||
assert.match(serialized, /credit = Night Shift/)
|
||||
assert.match(serialized, /website = https:\/\/themes\.example\/night-shift/)
|
||||
assert.match(serialized, /description = Soft neon palette for late sessions/)
|
||||
assert.equal(parsed.credit, 'Night Shift')
|
||||
assert.equal(parsed.website, 'https://themes.example/night-shift')
|
||||
assert.equal(parsed.description, 'Soft neon palette for late sessions')
|
||||
})
|
||||
|
||||
test('parseThemeFileContent maps legacy Astra sections into now playing tokens', () => {
|
||||
|
||||
Reference in New Issue
Block a user