From 8ceaafa6c8a8399e8d6e3d622b18692d38fd393e Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Sat, 18 Apr 2026 03:08:40 -0400 Subject: [PATCH] remove iro file association, show theme descriptions --- README.md | 2 +- package.json | 6 -- src/main/index.ts | 73 ------------------- src/preload/index.ts | 5 -- src/renderer/App.tsx | 6 -- src/renderer/components/BottomBar.tsx | 49 ++++++++----- .../components/NowPlayingConfigWindow.tsx | 7 -- src/renderer/env.d.ts | 1 - src/renderer/stores/themeStore.ts | 5 -- src/renderer/styles/globals.css | 16 +++- test/renderer-helpers.test.ts | 45 ++++++++++-- test/theme-library.test.ts | 5 +- 12 files changed, 91 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 15c98d8..08fb71e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index 027948c..7408a1b 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/main/index.ts b/src/main/index.ts index 95d05da..c94300a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -59,7 +59,6 @@ let nowPlayingConfigBoundsTimer: ReturnType | null = null const windowSettingsHeights = new Map() const windowSettingsBottomAnchors = new Map() 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 { } } -async function processPendingThemeOpenPaths(): Promise { - 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() } }) } diff --git a/src/preload/index.ts b/src/preload/index.ts index 6e26151..6358cc1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 6c323e9..c05c59d 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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, diff --git a/src/renderer/components/BottomBar.tsx b/src/renderer/components/BottomBar.tsx index 4ccaf75..3aecbd1 100644 --- a/src/renderer/components/BottomBar.tsx +++ b/src/renderer/components/BottomBar.tsx @@ -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):
Theme
{themeCredit.credit ? ( - themeCredit.url ? ( - { - event.preventDefault() - void handleOpenThemeWebsite(themeCredit.url!) - }} - > - By {themeCredit.credit} - - ) : ( - By {themeCredit.credit} - ) + + {themeCredit.url ? ( + { + event.preventDefault() + void handleOpenThemeWebsite(themeCredit.url!) + }} + > + By {themeCredit.credit} + + ) : ( + By {themeCredit.credit} + )} + {themeCredit.description ? ( + + + {themeCredit.description} + + ) : null} + ) : null}
diff --git a/src/renderer/components/NowPlayingConfigWindow.tsx b/src/renderer/components/NowPlayingConfigWindow.tsx index ac16482..9287915 100644 --- a/src/renderer/components/NowPlayingConfigWindow.tsx +++ b/src/renderer/components/NowPlayingConfigWindow.tsx @@ -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, diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts index b3dfcee..75a87b8 100644 --- a/src/renderer/env.d.ts +++ b/src/renderer/env.d.ts @@ -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 diff --git a/src/renderer/stores/themeStore.ts b/src/renderer/stores/themeStore.ts index f469473..4b05c77 100644 --- a/src/renderer/stores/themeStore.ts +++ b/src/renderer/stores/themeStore.ts @@ -20,7 +20,6 @@ interface ThemeState { activeTheme: PrismResolvedTheme accent: string initializeThemes: () => Promise - applyExternalThemeSnapshot: (snapshot: ThemeLibrarySnapshot) => void loadTheme: (id: string) => Promise renameTheme: (id: string, name: string) => Promise deleteTheme: (id: string) => Promise @@ -104,10 +103,6 @@ export const useThemeStore = create((set) => ({ applyThemeSnapshot(set, snapshot) }, - applyExternalThemeSnapshot: (snapshot) => { - applyThemeSnapshot(set, snapshot) - }, - loadTheme: async (id: string) => { if (!canUseElectronAPI()) return const snapshot = await window.electronAPI.loadTheme(id) diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index 90b284e..126176e 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -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; diff --git a/test/renderer-helpers.test.ts b/test/renderer-helpers.test.ts index ac7fae0..519c72a 100644 --- a/test/renderer-helpers.test.ts +++ b/test/renderer-helpers.test.ts @@ -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 \{/) }) diff --git a/test/theme-library.test.ts b/test/theme-library.test.ts index b0c490c..f4ff9c3 100644 --- a/test/theme-library.test.ts +++ b/test/theme-library.test.ts @@ -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', () => {