the player moves to the plugin, and src/ has no music code left
officerdev/src/MusicPlayer/ → plugins/music/web/. Engine, state, bar, favourites, lyrics toggle and the library vocabulary — ten files. The barrel stops exporting a player it no longer has, and DashboardLayout stops rendering one. The reasoning that kept it was removed rather than refuted. It stayed because the dashboard widget imported useMusicPlayer from officerdev and the platform cannot import from a plugin, so the state had to stay whatever was decided about the UI. The owner moved the widget into the plugin in the previous commit, and the constraint went with it: the whole remaining dependency became one line, DashboardLayout.tsx:66. MusicPlayerHost is mounted inside the MusicDetail panel. That reads odd until you notice it already returned null on /music — the mini bar is the transport there, and the host existed purely to own the GaplessEngine. In the panel it does exactly that, and the bar code stays intact for whenever there is a slot. [phase 2] Leaving /music unmounts the host and playback stops. Deferred on the owner's call; the bar was "navigating away must not break the application", and that holds: seekPlayer is optional-chained so a call with no host registered is a no-op, registerPlayerSeek clears only its own registration, the host's cleanup destroys the engine and nulls its ref, and the queue is global state so returning to /music remounts and reloads. Solving it properly needs either a shell slot a plugin can contribute to — which reopens "there is no way to export a component" — or the engine hoisted to module scope, which keeps the rule and loses only the off-route controls. Also: the parked widget now imports the player as a sibling rather than through officerdev, and shared.ts stopped being a re-export shim now that the real file is in the plugin. Verified: tsgo clean, 797 tests / 787 pass / same 7. Server restarts, mounts /example /music /offscale, / and /music both 200, and the player is in the built bundle (music.volume, music:lyrics, now-playing?device=web all present — GaplessEngine is a class name and the production build is minified, so grepping for it proves nothing). Not verified by me: what it looks like in a browser. That needs your eyes.
This commit is contained in:
+28
-23
@@ -91,36 +91,41 @@ to know about provenance. It now calls `mountPrefix`.
|
||||
the platform imports the module and reads `router`, so there is nowhere to inject it. The fix is
|
||||
`api/router.ts` exporting a factory the installer calls with the plugin's own identity.
|
||||
|
||||
### 3. The player — the one open judgement call, and it is decided
|
||||
### 3. ~~The player~~ — moved, and the reasoning that kept it was removed rather than refuted
|
||||
|
||||
**`officerdev/src/MusicPlayer/` stays in the platform.** The runbook left this open with either answer
|
||||
acceptable. What decided it was not the overlay but the state:
|
||||
The first version of this document said the player stayed in the platform and called the decision
|
||||
settled by a hard constraint:
|
||||
|
||||
> `useMusicPlayer` and `PlayerTrack` are imported from `officerdev` by
|
||||
> `src/workspaces/widgets/MusicPlayer/`, the dashboard widget — which is _also_ out of scope and stays.
|
||||
> **The platform cannot import from a plugin.** So the player state stays here whatever is decided about
|
||||
> the UI around it, and a second copy would mean two audio engines fighting over one pair of speakers.
|
||||
> `useMusicPlayer` and `PlayerTrack` are imported from `officerdev` by `widgets/MusicPlayer/`, the
|
||||
> dashboard widget — and the platform cannot import from a plugin. So the player state stays whatever is
|
||||
> decided about the UI.
|
||||
|
||||
Given the state had to stay, splitting the engine and the bar away from the thing they drive would have
|
||||
left the same seam in a worse place. And moving them needed a shell slot that renders a plugin-provided
|
||||
component on **every route** — which is exactly the escape hatch this system deleted on purpose. "There
|
||||
is no way to export a component" is what makes "every plugin route is a Workspace" a property of the
|
||||
shape rather than a rule someone has to remember, and reopening it for one plugin is a bad trade.
|
||||
True at the time. The owner then moved the widget into the plugin, and the constraint evaporated: the
|
||||
complete remaining platform dependency became one line, `DashboardLayout.tsx:66`.
|
||||
|
||||
The seam is inert without the plugin: `MusicPlayerHost` gates on `can('music')`, and `music` is now the
|
||||
plugin's permission — registered at install, gone at uninstall.
|
||||
So the whole of `officerdev/src/MusicPlayer/` now lives in `web/` — engine, state, bar, favourites,
|
||||
lyrics toggle and the library vocabulary. **`src/` contains no music code at all.**
|
||||
|
||||
What stayed with it, and why each: `gapless-engine` (the engine the state drives), `player-time` (the
|
||||
module-level bridge the lyrics pane meets it through), `useLyricsOpen` and `MusicHeart` +
|
||||
`useMusicFavorites` (the bar renders a heart), and `shared.ts` — the library vocabulary, which the host
|
||||
needs a third of and the plugin needs all of. One definition on the host side beats a copy either side
|
||||
of the boundary drifting apart; `plugins/music/web/shared.ts` re-exports it from the package's declared
|
||||
`officerdev/MusicPlayer/shared` subpath.
|
||||
**Where the engine is mounted, and why it is not the shell.** `MusicPlayerHost` renders inside
|
||||
`MusicDetail`, at the foot of the library view. That reads odd until you notice what it already did:
|
||||
|
||||
**What would close it:** the widget learning to come from a plugin. Not the overlay slot — that one
|
||||
should stay shut.
|
||||
```tsx
|
||||
if (pathname.startsWith('/music')) return null; // the panel draws its own MusicMiniBar
|
||||
```
|
||||
|
||||
---
|
||||
On `/music` the host has always rendered nothing and existed purely to own the `GaplessEngine`. Mounted
|
||||
in the panel it does exactly that, and the bar code stays intact for whenever there is a slot to put it in.
|
||||
|
||||
**`[phase 2]` Leaving `/music` unmounts the host, which stops playback.** Deliberately deferred rather
|
||||
than solved: making audio outlive the route needs either a shell slot a plugin can contribute to — which
|
||||
reopens "there is no way to export a component" — or the engine hoisted to module scope so a panel
|
||||
attaches and detaches from a singleton. The second keeps the rule and loses only the off-route transport
|
||||
controls, and is the better idea, but it is a rewrite of the host's lifecycle rather than a move.
|
||||
|
||||
Nothing breaks in the meantime, and that was the bar: `player-time`'s `seekPlayer` is optional-chained
|
||||
so a call with no host registered is a no-op, `registerPlayerSeek` clears only its own registration, the
|
||||
host's cleanup destroys the engine and nulls its ref, and the queue lives in global state — so returning
|
||||
to `/music` remounts the host and reloads it.
|
||||
|
||||
## Two bugs, neither visible from reading
|
||||
|
||||
|
||||
Reference in New Issue
Block a user