Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.

Commit 37f7321

Browse files
authored
Merge pull request #35 from NuclearPlayer/feat/dashboard
Dashboard
2 parents 877e085 + 392fb85 commit 37f7321

File tree

85 files changed

+2952
-336
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+2952
-336
lines changed

AGENTS.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ Tests use Vitest + React Testing Library. Globals enabled (`describe`, `it`, `ex
193193
- When semantic queries aren't possible, add `data-testid` attributes. And don't be shy with them
194194
- Don't use defensive measures like try-catch or conditional checks in tests. The test will fail anyway if our assumptions are wrong.
195195

196+
### Test-first for views
197+
198+
When building a new view, write the test wrapper and tests **before** any implementation code. The tests describe what the user sees and does — they define the contract. Then implement to make them pass.
199+
200+
Don't start with unit tests for internal utilities (grouping functions, registries, etc.). Start from the outside: what does the user see on the page? The internal structure is an implementation detail that falls out of making the tests green.
201+
196202
### Test Wrappers for Views
197203

198204
Player views and some components use a `*.test-wrapper.tsx` file that creates a domain-specific abstraction layer over the DOM. This lets tests read like user stories, and if the implementation changes, only the wrapper needs updating.
@@ -206,6 +212,43 @@ Player views and some components use a `*.test-wrapper.tsx` file that creates a
206212
- Don't use queryX methods in the wrapper - always get or find as appropriate.
207213
- Never use fireEvent. Always use userEvent for interactions.
208214

215+
### Test fixtures
216+
217+
To populate the app with testing data, use fixtures. See `packages/player/src/test/fixtures` for examples.
218+
219+
### Wrapper fixtures
220+
221+
Test wrappers can expose a `fixtures` object with factory methods that return pre-configured builders for common test scenarios. This keeps test setup readable and co-located with the wrapper, while the raw fixture data itself lives in `packages/player/src/test/fixtures/`.
222+
223+
```tsx
224+
// Dashboard.test-wrapper.tsx
225+
import { TOP_TRACKS_RADIOHEAD } from '../../test/fixtures/dashboard';
226+
227+
export const DashboardWrapper = {
228+
// ... mount, getters, etc.
229+
230+
fixtures: {
231+
topTracksProvider() {
232+
return new DashboardProviderBuilder()
233+
.withCapabilities('topTracks')
234+
.withFetchTopTracks(async () => TOP_TRACKS_RADIOHEAD);
235+
},
236+
},
237+
};
238+
239+
// Dashboard.test.tsx
240+
DashboardWrapper.seedProvider(DashboardWrapper.fixtures.topTracksProvider());
241+
```
242+
243+
### The builder pattern for tests
244+
245+
We use builders to create test data and various entities cleanly. You can see them in `packages/player/src/test/builders`.
246+
247+
- A builder is a class that has an instance of the object it's building
248+
- When the builder is instantiated, it creates a default object with reasonable defaults
249+
- The builder has methods that mutate the object and return `this` for chaining
250+
- The `build()` method returns the final object, which can then be used in tests
251+
209252
```tsx
210253
// Playlists.test-wrapper.tsx
211254
export const PlaylistsWrapper = {

packages/docs/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: Nuclear Music Player. Play the web!
2+
description: Nuclear Music Player docs
33
---
44

55
# Nuclear Documentation
@@ -19,7 +19,7 @@ Nuclear is a free, open‑source music player that acts as a hub for many servic
1919
## For users
2020

2121
- Themes: Nuclear supports theming. You can load your own JSON themes or choose built‑ins.
22-
- Plugins: Extend Nuclear with plugins. There is no sandboxplugins can control the player directly. Only install plugins you trust.
22+
- Plugins: Extend Nuclear with plugins. There is no sandbox, plugins can control the player directly. Only install plugins you trust.
2323

2424
## What is in this repo?
2525

@@ -42,7 +42,7 @@ This is a pnpm/turbo monorepo with these major packages:
4242
- TypeScript everywhere
4343
- Tauri (desktop shell)
4444
- React 18
45-
- Tailwind v4 configured via CSS (@theme/@layer) no tailwind.config.js
45+
- Tailwind v4 configured via CSS (@theme/@layer), no tailwind.config.js
4646
- TanStack Router (routing)
4747
- TanStack Query v5 (HTTP and client‑side server state; no backend server)
4848
- Vitest + React Testing Library (tests)

packages/docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Favorites](plugins/favorites.md)
1010
* [Streaming](plugins/streaming.md)
1111
* [Metadata](plugins/metadata.md)
12+
* [Dashboard](plugins/dashboard.md)
1213
* [Providers](plugins/providers.md)
1314

1415
## Theming

packages/docs/plugins/dashboard.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
description: Supply dashboard content: top tracks, trending artists, curated playlists, and more.
3+
---
4+
5+
# Dashboard
6+
7+
## Dashboard Providers
8+
9+
Dashboard providers supply Nuclear's home screen with content like charts, trending artists, new releases, and curated playlists. When you open the player, Nuclear calls each registered dashboard provider and assembles the results into an overview. Without a dashboard provider, the dashboard shows an empty state prompting the user to enable a plugin.
10+
11+
Plugins can either add a new dashboard provider, or consume dashboard data from existing providers.
12+
13+
---
14+
15+
## Implementing a provider
16+
17+
### Minimal example
18+
19+
You register dashboard providers with `api.Providers.register()` just like any other provider. It needs an `id`, `kind: 'dashboard'`, a `name`, a `metadataProviderId`, and a list of `capabilities` declaring which content it can supply:
20+
21+
```typescript
22+
import type { DashboardProvider, NuclearPlugin, NuclearPluginAPI } from '@nuclearplayer/plugin-sdk';
23+
24+
const provider: DashboardProvider = {
25+
id: 'acme-dashboard',
26+
kind: 'dashboard',
27+
name: 'Acme Music',
28+
29+
metadataProviderId: 'acme-metadata',
30+
capabilities: ['topTracks', 'newReleases'],
31+
32+
async fetchTopTracks() {
33+
// Call your API, return Track[]
34+
},
35+
async fetchNewReleases() {
36+
// Call your API, return AlbumRef[]
37+
},
38+
};
39+
40+
const plugin: NuclearPlugin = {
41+
onEnable(api: NuclearPluginAPI) {
42+
api.Providers.register(provider);
43+
},
44+
onDisable(api: NuclearPluginAPI) {
45+
api.Providers.unregister('acme-dashboard');
46+
},
47+
};
48+
49+
export default plugin;
50+
```
51+
52+
{% hint style="warning" %}
53+
Always unregister your provider in `onDisable`. If you don't, it stays registered and Nuclear will keep calling it after the plugin is disabled.
54+
{% endhint %}
55+
56+
### Capabilities
57+
58+
Capabilities tell Nuclear which content your provider can fetch. Nuclear only calls methods for capabilities you declare. If you don't list `'topArtists'`, `fetchTopArtists` is never called even if you define it.
59+
60+
| Capability | Method called | Returns | Widget |
61+
|------------|--------------|---------|--------|
62+
| `'topTracks'` | `fetchTopTracks()` | `Track[]` | Track table with playback controls |
63+
| `'topArtists'` | `fetchTopArtists()` | `ArtistRef[]` | Artist card row |
64+
| `'topAlbums'` | `fetchTopAlbums()` | `AlbumRef[]` | Album card row |
65+
| `'editorialPlaylists'` | `fetchEditorialPlaylists()` | `PlaylistRef[]` | Playlist card row |
66+
| `'newReleases'` | `fetchNewReleases()` | `AlbumRef[]` | Album card row |
67+
68+
{% hint style="info" %}
69+
Nuclear only calls methods for declared capabilities. Declare only the ones you actually implement.
70+
{% endhint %}
71+
72+
Note that `fetchTopTracks` returns full `Track[]` objects, not `TrackRef[]`. The dashboard track table needs complete track data (title, artist, duration, thumbnail) to render without additional lookups.
73+
74+
---
75+
76+
## The `metadataProviderId` field
77+
78+
Every dashboard provider must specify a `metadataProviderId`. This tells Nuclear which metadata provider can look up the entities your dashboard returns.
79+
80+
When a user clicks an artist or album on the dashboard, Nuclear navigates to a detail page. It needs to know which metadata provider can fetch that entity's full details, that's what `metadataProviderId` is for. If you set it to a provider that doesn't exist or can't resolve your IDs, artist and album links from the dashboard will fail.
81+
82+
Typically, your plugin registers both a metadata provider and a dashboard provider, and they share the same underlying API. Point `metadataProviderId` at your metadata provider's `id`.
83+
84+
---
85+
86+
## Attributed results
87+
88+
When multiple dashboard providers are registered, Nuclear will render results from all of them. The API wraps each provider's data in an `AttributedResult<T>`:
89+
90+
```typescript
91+
type AttributedResult<T> = {
92+
providerId: string;
93+
metadataProviderId: string;
94+
providerName: string;
95+
items: T[];
96+
};
97+
```
98+
99+
Each attributed result carries the provider's name and IDs, so Nuclear can render labeled sections (e.g. "Top Tracks - Acme Music") and route navigation to the correct metadata provider.
100+
101+
---
102+
103+
## Using dashboard data
104+
105+
Plugins can consume dashboard data from all registered providers via `api.Dashboard.*`:
106+
107+
```typescript
108+
export default {
109+
async onEnable(api: NuclearPluginAPI) {
110+
const topTracks = await api.Dashboard.fetchTopTracks();
111+
112+
for (const result of topTracks) {
113+
console.log(`${result.providerName}: ${result.items.length} tracks`);
114+
}
115+
},
116+
};
117+
```
118+
119+
### Consumer reference
120+
121+
```typescript
122+
api.Dashboard.fetchTopTracks(providerId?: string): Promise<AttributedResult<Track>[]>
123+
api.Dashboard.fetchTopArtists(providerId?: string): Promise<AttributedResult<ArtistRef>[]>
124+
api.Dashboard.fetchTopAlbums(providerId?: string): Promise<AttributedResult<AlbumRef>[]>
125+
api.Dashboard.fetchEditorialPlaylists(providerId?: string): Promise<AttributedResult<PlaylistRef>[]>
126+
api.Dashboard.fetchNewReleases(providerId?: string): Promise<AttributedResult<AlbumRef>[]>
127+
```
128+
129+
All methods accept an optional `providerId`. If omitted, Nuclear queries all registered dashboard providers and returns an array with one `AttributedResult` per provider. If provided, only that provider is queried.
130+
131+
---

packages/docs/plugins/favorites.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Each favorite entry includes a timestamp (`addedAtIso`) recording when it was ad
3030

3131
### Identity and deduplication
3232

33-
Favorites are identified by their `source` field—a combination of provider name and ID. Because each metadata provider stores music data differently, **favorites from each provider are treated separately**.
33+
Favorites are identified by their `source` fiels. It's a combination of provider name and ID. Because each metadata provider stores music data differently, **favorites from each provider are treated separately**.
3434

3535
```typescript
3636
type ProviderRef = {

packages/docs/plugins/providers.md

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,79 @@
11
---
2-
description: Extend Nuclear with custom metadata providers through the Providers API.
2+
description: Register providers that supply metadata, audio streams, dashboard content, and more to Nuclear.
33
---
44

55
# Providers
66

77
## Providers API for Plugins
88

9-
The Providers API allows plugins to register custom providers that supply metadata for tracks, albums, and artists. This enables Nuclear to integrate with any music service or database.
9+
Providers are modules that fulfill specific data requests from Nuclear. When the player needs to search for tracks, stream audio, or populate the dashboard, it queries the providers for that kind of request. Different provider kinds serve different purposes, and plugins can register as many providers as they need.
10+
11+
All registration and lookup goes through `api.Providers`.
1012

1113
{% hint style="info" %}
12-
Access providers via `NuclearAPI.Providers.*` in your plugin's lifecycle hooks. Only metadata providers are supported.
14+
Every provider extends the `ProviderDescriptor` base type, which requires an `id`, `kind`, and `name`. Each provider kind then adds its own domain-specific methods (e.g., `search` for metadata, `getStreamUrl` for streaming).
1315
{% endhint %}
1416

15-
---
17+
## Provider kinds
18+
19+
| Kind | Purpose | Guide |
20+
|------|---------|-------|
21+
| `'metadata'` | Search results, artist & album details | [Metadata](metadata.md) |
22+
| `'streaming'` | Audio stream URLs for playback | [Streaming](streaming.md) |
23+
| `'dashboard'` | Dashboard content (top tracks, new releases, etc.) | [Dashboard](dashboard.md) |
24+
| `'lyrics'` | Song lyrics *(planned)* | N/A |
25+
26+
## Registration
27+
28+
Register providers in your plugin's `onEnable` hook and unregister them in `onDisable`:
29+
30+
```ts
31+
import type { NuclearPluginAPI, MetadataProvider } from '@nuclearplayer/plugin-sdk';
32+
33+
let providerId: string;
34+
35+
export default {
36+
onEnable(api: NuclearPluginAPI) {
37+
const provider: MetadataProvider = {
38+
id: 'my-metadata-source',
39+
kind: 'metadata',
40+
name: 'My Metadata Source',
41+
search: async (query) => { /* ... */ },
42+
fetchArtist: async (artistId) => { /* ... */ },
43+
fetchAlbum: async (albumId) => { /* ... */ },
44+
};
45+
46+
providerId = api.Providers.register(provider);
47+
},
48+
49+
onDisable(api: NuclearPluginAPI) {
50+
api.Providers.unregister(providerId);
51+
},
52+
};
53+
```
54+
55+
`register()` returns the provider's ID, which you pass to `unregister()` later.
56+
57+
## Provider lifecycle
58+
59+
Always register in `onEnable` and unregister in `onDisable`. If you skip unregistration, the provider stays in Nuclear's registry after your plugin is disabled, and the player may still pass queries to that phantom provider.
1660

17-
## Core concepts
61+
| Hook | Action |
62+
|------|--------|
63+
| `onEnable` | `api.Providers.register(provider)` |
64+
| `onDisable` | `api.Providers.unregister(providerId)` |
1865

19-
### What are providers?
66+
## Base type
2067

21-
Providers are modules that fulfill specific data requests from Nuclear. When the app needs information (like album art, track metadata, or artist details), it queries the selected provider.
68+
All providers share this shape:
2269

23-
### Provider types
70+
```ts
71+
type ProviderDescriptor<K extends ProviderKind = ProviderKind> = {
72+
id: string;
73+
kind: K;
74+
name: string;
75+
pluginId?: string;
76+
};
77+
```
2478

25-
**Metadata Providers** (current)
26-
- Supply information about tracks, albums, and artists
27-
- Examples: MusicBrainz, Last.fm, Discogs
28-
- Used for search, library management, and display
79+
Each provider kind (e.g., `MetadataProvider`, `StreamingProvider`, `DashboardProvider`) extends `ProviderDescriptor` with its own methods. See the individual guides linked above for details.

packages/i18n/src/locales/en_US.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,18 @@
144144
},
145145
"dashboard": {
146146
"title": "Dashboard",
147-
"content": "Dashboard view"
147+
"empty-state": "No dashboard data available",
148+
"empty-state-description": "Enable a plugin that provides dashboard content.",
149+
"top-tracks": "Top Tracks",
150+
"top-artists": "Top Artists",
151+
"top-albums": "Top Albums",
152+
"editorial-playlists": "Top Playlists",
153+
"new-releases": "New Releases",
154+
"filter-artists": "Filter artists...",
155+
"filter-albums": "Filter albums...",
156+
"filter-playlists": "Filter playlists...",
157+
"filter-releases": "Filter releases...",
158+
"nothing-found": "Nothing found"
148159
},
149160
"plugins": {
150161
"title": "Plugins",

packages/player/src-tauri/src/main.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ fn main() {
1010

1111
#[cfg(target_os = "linux")]
1212
fn apply_linux_workarounds() {
13-
// Fix for WebKitGTK DMA-BUF renderer causing "Could not create default EGL display:
14-
// EGL_BAD_PARAMETER" on various Linux systems including Steam Deck, NVIDIA GPUs,
15-
// and certain Wayland compositors.
13+
// Fix for WebKitGTK GPU rendering failures on various Linux systems including
14+
// Steam Deck, NVIDIA GPUs, and W**land compositors.
15+
//
16+
// WEBKIT_DISABLE_DMABUF_RENDERER: Fixes "Could not create default EGL display:
17+
// EGL_BAD_PARAMETER" and blank screens on NVIDIA.
1618
// See: https://github.com/tauri-apps/tauri/issues/9394
19+
//
20+
// WEBKIT_DISABLE_COMPOSITING_MODE: Fixes "Failed to get GBM device" errors
21+
// and crashes on resize with NVIDIA/Steam Deck.
22+
// See: https://github.com/tauri-apps/tauri/issues/11994
1723
if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() {
1824
unsafe {
1925
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
2026
}
2127
}
28+
if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").is_err() {
29+
unsafe {
30+
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
31+
}
32+
}
2233
}

0 commit comments

Comments
 (0)