feat(plugins): full-screen (immersive) plugin screens via manifest opt-in (#590)

DAW-style plugin UIs (e.g. a practice studio) need the whole viewport, not a
scrolling content page below the v3 topbar — embedded in the shell they get
cut off at the bottom with excess padding up top.

Add an opt-in top-level `"fullscreen": true` plugin.json field, surfaced as the
`fullscreen` boolean on /api/plugins (mirrors the settings_category plumbing in
plugins/__init__.py). When a fullscreen plugin's screen is active, static/v3/
shell.js toggles `html.fb-immersive` from syncActive() so it tracks every
navigation incl. deep-link; static/v3/v3.css then hides the topbar, collapses
the sidebar to a functional icon rail (kept reachable — Escape is bound only on
player/settings scopes, so a fully hidden sidebar would trap the user), and
lets the active plugin screen fill #v3-main. Mirrors the existing
ss-follower-pre chrome-hide pattern. Additive + opt-in: plugins without the
flag are unaffected.

Test: tests/test_plugins.py::test_fullscreen_flag_parsed_from_manifest


Claude-Session: https://claude.ai/code/session_01BmWopMsRjdZyD6RwmZAQBv

Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
ChrisBeWithYou
2026-06-25 00:02:16 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent 8b4c9b0050
commit 4c3ec2ff66
5 changed files with 106 additions and 2 deletions
+32
View File
@@ -4035,3 +4035,35 @@ def test_settings_category_parsed_from_manifest(tmp_path, reset_plugin_state):
assert rows["plainset"]["has_settings"] is True
assert rows["noset"]["settings_category"] is None
assert rows["noset"]["has_settings"] is False
def test_fullscreen_flag_parsed_from_manifest(tmp_path, reset_plugin_state):
"""A plugin manifest's top-level `fullscreen: true` surfaces as the boolean
`fullscreen` on the loaded entry (drives the v3 shell's immersive mode).
Only a strict boolean `true` opts in — absent, false, or a truthy non-bool
(e.g. the string "true") all resolve to False so a plugin can't be opted in
by accident."""
plugins = reset_plugin_state
def _write(pid, manifest_extra):
d = tmp_path / pid
d.mkdir()
(d / "plugin.json").write_text(json.dumps({
"id": pid, "name": pid, "routes": "routes.py",
"screen": "screen.html", **manifest_extra,
}))
(d / "routes.py").write_text("def setup(app, ctx):\n pass\n")
(d / "screen.html").write_text("<div></div>")
_write("immersive", {"fullscreen": True})
_write("strflag", {"fullscreen": "true"}) # truthy non-bool → not opted in
_write("falseflag", {"fullscreen": False})
_write("noflag", {}) # field absent
_run_load_plugins(plugins, type("FakeApp", (), {})(), tmp_path)
rows = {p["id"]: p for p in plugins.LOADED_PLUGINS}
assert rows["immersive"]["fullscreen"] is True
assert rows["strflag"]["fullscreen"] is False
assert rows["falseflag"]["fullscreen"] is False
assert rows["noflag"]["fullscreen"] is False