mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
feat(vegas): one declared participation per plugin (scroll | pause | exclude) (#682)
A plugin takes part in Vegas mode in one declared way: 'scroll', 'pause' or 'exclude', resolved from the user's vegas_participation setting, the manifest field, then the legacy hooks, so no plugin changes behaviour. The stream manager decides inclusion and pauses through it; the installed plugins API and the Vegas plugin-order list report it. Deprecates get_supported_vegas_modes, get_vegas_segment_width and vegas_panel_count for removal in 3.9.0, and regenerates docs/DEPRECATIONS_3.8.md to include them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,46 @@ accepts both, but the store flags the old spelling as deprecated
|
||||
`POST /api/v3/auth/password`, `POST /api/v3/auth/disable`,
|
||||
`GET|POST /api/v3/auth/tokens`, `DELETE /api/v3/auth/tokens/<id>`.
|
||||
|
||||
### Vegas participation
|
||||
|
||||
A plugin now takes part in Vegas mode in one declared way: `'scroll'` (its
|
||||
content scrolls by), `'pause'` (the scroll stops for its turn and its
|
||||
`display()` draws it full screen) or `'exclude'`. No plugin changes
|
||||
behaviour: one that declares nothing gets exactly what the old hooks gave
|
||||
it, checked against every official plugin.
|
||||
|
||||
- `BasePlugin.get_vegas_participation()` resolves, in order: the user's
|
||||
`vegas_participation` config value, the manifest's `vegas_participation`,
|
||||
then the legacy hooks (`get_vegas_display_mode()` returning `STATIC` →
|
||||
pause, else `get_vegas_content_type()` returning `'none'` → exclude, else
|
||||
scroll). `resolve_vegas_participation()` in `src.plugin_system.base_plugin`
|
||||
is what the core calls; the user's setting wins even over a plugin that
|
||||
overrides the method.
|
||||
- The Vegas stream manager decides inclusion and pauses through it, and
|
||||
`PluginAdapter.get_content_type()` is removed (core-internal, now unused).
|
||||
Swap mode no longer drops a plugin's segment for a cycle when its
|
||||
`get_vegas_display_mode()` raises something other than
|
||||
`AttributeError`/`TypeError`: like every other decision point it now
|
||||
treats that as "not paused".
|
||||
- `vegas_participation` is a core-owned per-plugin property (an enum with no
|
||||
default) and a manifest field in `schema/manifest_schema.json`.
|
||||
- `GET /api/v3/plugins/installed` reports each plugin's
|
||||
`vegas_participation`, and the Vegas plugin-order list badges it (Scroll /
|
||||
Pause / Excluded) instead of the old Scroll / Fixed / Static.
|
||||
- `src.deprecation.warn_deprecated()` warns once per process for what
|
||||
`@deprecated` cannot decorate, such as a config key.
|
||||
|
||||
Deprecated, removed in 3.9.0 (each logs a warning on first use). Vegas never
|
||||
read any of them:
|
||||
|
||||
- `BasePlugin.get_supported_vegas_modes()` and
|
||||
`BasePlugin.get_vegas_segment_width()`.
|
||||
- The `vegas_panel_count` per-plugin setting (warns once per plugin that sets
|
||||
it).
|
||||
- The SCROLL / FIXED_SEGMENT distinction (`vegas_mode` `"scroll"` vs
|
||||
`"fixed"`): both always scrolled. Documented only; no warning, because
|
||||
official plugins' schemas still offer `"fixed"`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- On-demand no longer restarts a running display. `POST
|
||||
|
||||
+70
-68
@@ -10,22 +10,27 @@ This guide covers advanced LEDMatrix features for users and developers, includin
|
||||
|
||||
Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to news tickers seen in Las Vegas casinos. Plugins contribute content segments that flow across the display in a seamless ticker-style presentation.
|
||||
|
||||
### Display Modes
|
||||
### How a Plugin Takes Part
|
||||
|
||||
**SCROLL (Continuous Scrolling):**
|
||||
- Content scrolls continuously left
|
||||
- Smooth, fluid motion
|
||||
- Best for news-ticker style displays
|
||||
Each plugin has a *Vegas participation*:
|
||||
|
||||
**FIXED_SEGMENT (Fixed-Width Block):**
|
||||
- Plugin gets fixed-width block on display
|
||||
- Content doesn't scroll out of its segment
|
||||
- Multiple plugins can share the display simultaneously
|
||||
**`scroll` (the default):**
|
||||
- The plugin's content scrolls by with everyone else's
|
||||
- Best for news-ticker style content: scores, headlines, prices, the time
|
||||
|
||||
**STATIC (Scroll Pauses):**
|
||||
- Scrolling pauses when content is fully visible
|
||||
- Displays for specified duration, then resumes scrolling
|
||||
- Best for content that needs to be fully read
|
||||
**`pause`:**
|
||||
- The scroll stops when the plugin's turn comes round
|
||||
- The plugin draws the whole panel for its display duration, then the
|
||||
scroll resumes
|
||||
- Best for content that needs to be read in full, or alerts
|
||||
|
||||
**`exclude`:**
|
||||
- The plugin is left out of Vegas mode
|
||||
|
||||
A plugin declares its default; set `vegas_participation` in a plugin's
|
||||
config to override it (see [Per-Plugin Configuration](#per-plugin-configuration)).
|
||||
Older documentation also describes a *fixed segment* mode; Vegas never
|
||||
implemented one, and it has always behaved exactly like `scroll`.
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -164,8 +169,7 @@ Override Vegas behavior for specific plugins:
|
||||
{
|
||||
"my_plugin": {
|
||||
"enabled": true,
|
||||
"vegas_mode": "scroll",
|
||||
"vegas_panel_count": 2,
|
||||
"vegas_participation": "pause",
|
||||
"display_duration": 10
|
||||
}
|
||||
}
|
||||
@@ -175,19 +179,30 @@ Override Vegas behavior for specific plugins:
|
||||
|
||||
| Setting | Values | Description |
|
||||
|---------|--------|-------------|
|
||||
| `vegas_mode` | `scroll`, `fixed`, `static` | Display mode for this plugin |
|
||||
| `vegas_panel_count` | any positive integer | Width in panels (1 panel = display width) |
|
||||
| `display_duration` | seconds | Pause duration for STATIC mode |
|
||||
| `vegas_participation` | `scroll`, `pause`, `exclude` | How this plugin takes part: its content scrolls by, the scroll pauses for its turn and shows it full screen, or it is left out. Unset uses the plugin's own default |
|
||||
| `display_duration` | seconds | How long a `pause` plugin holds the screen |
|
||||
| `vegas_width_pct` | 10–100 | Width of this plugin's card, as a percentage of the panel |
|
||||
| `vegas_overflow` | `rotate`, `truncate` | What to do when its content is wider than its allowance |
|
||||
| `vegas_max_width_screens` | number of screens | The widest its card may be |
|
||||
|
||||
Plugins may also set `vegas_overflow` and `vegas_max_width_screens` in
|
||||
their config section to control how oversized content is handled (see
|
||||
`PluginManager` in `src/plugin_system/plugin_manager.py`).
|
||||
These are core-owned settings (see
|
||||
[PLUGIN_CONFIG_CORE_PROPERTIES.md](PLUGIN_CONFIG_CORE_PROPERTIES.md)): every
|
||||
plugin accepts them whether or not its own schema lists them. Set them in
|
||||
the plugin's section of config.json, in the web UI's **Config Editor**
|
||||
tab.
|
||||
|
||||
Some plugins also offer a `vegas_mode` setting of their own (`scroll`,
|
||||
`fixed` or `static`). It still works — `static` pauses, the other two scroll
|
||||
— but `vegas_participation` takes precedence, and `fixed` has never done
|
||||
anything different from `scroll`. The old `vegas_panel_count` setting never
|
||||
had an effect and is deprecated (removed in 3.9.0).
|
||||
|
||||
### Plugin Integration (Developer Guide)
|
||||
|
||||
All of these have defaults in
|
||||
[`BasePlugin`](../src/plugin_system/base_plugin.py); override only what you
|
||||
need.
|
||||
need. The reference is
|
||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-scroll-hooks).
|
||||
|
||||
**1. Implement Content Method:**
|
||||
|
||||
@@ -203,43 +218,41 @@ If it returns `None` (the default), Vegas falls back to the plugin's
|
||||
(`PluginAdapter.get_content()` in
|
||||
[`src/vegas_mode/plugin_adapter.py`](../src/vegas_mode/plugin_adapter.py)).
|
||||
|
||||
**2. Specify Content Type:**
|
||||
**2. Declare how the plugin takes part:**
|
||||
|
||||
```python
|
||||
def get_vegas_content_type(self):
|
||||
# 'multi' | 'static' | 'none' -- default is 'static'
|
||||
return 'multi'
|
||||
Most plugins need nothing: the default is `scroll`. A plugin that should
|
||||
pause the scroll, or stay out of Vegas, says so in `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"vegas_participation": "pause"
|
||||
}
|
||||
```
|
||||
|
||||
`'none'` excludes the plugin from Vegas mode.
|
||||
|
||||
**3. Optionally Specify Display Mode:**
|
||||
|
||||
These return `VegasDisplayMode` members, not strings:
|
||||
The user's own `vegas_participation` setting overrides the manifest. When
|
||||
the answer depends on state, override the method instead:
|
||||
|
||||
```python
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode
|
||||
|
||||
def get_vegas_display_mode(self):
|
||||
return VegasDisplayMode.SCROLL
|
||||
|
||||
def get_supported_vegas_modes(self):
|
||||
return [VegasDisplayMode.SCROLL, VegasDisplayMode.STATIC]
|
||||
def get_vegas_participation(self):
|
||||
# 'scroll' | 'pause' | 'exclude'
|
||||
return 'pause' if self._alert_is_live() else 'scroll'
|
||||
```
|
||||
|
||||
`VegasDisplayMode` has `SCROLL` (`"scroll"`), `FIXED_SEGMENT` (`"fixed"`) and
|
||||
`STATIC` (`"static"`). The default `get_vegas_display_mode()` uses the
|
||||
plugin's `vegas_mode` config value if set, otherwise maps the content type
|
||||
(`multi` to `SCROLL`, anything else to `FIXED_SEGMENT`).
|
||||
A plugin written for an older core that declares nothing keeps its
|
||||
behaviour: `get_vegas_display_mode()` returning `VegasDisplayMode.STATIC`
|
||||
pauses, `get_vegas_content_type()` returning `'none'` excludes, and
|
||||
everything else scrolls. `get_supported_vegas_modes()`,
|
||||
`get_vegas_segment_width()` and the SCROLL / FIXED_SEGMENT distinction are
|
||||
deprecated (removed in 3.9.0): Vegas never read them.
|
||||
|
||||
### Content Rendering Guidelines
|
||||
|
||||
**Image Dimensions:**
|
||||
- **Height:** Must match display height (typically 32 pixels)
|
||||
- **Width:** Varies by mode:
|
||||
- SCROLL: Any width (recommended 64-512 pixels)
|
||||
- FIXED_SEGMENT: `panel_count * display_width`
|
||||
- STATIC: Any width, optimized for readability
|
||||
- **Width:** Any width for `scroll` (recommended 64-512 pixels);
|
||||
`get_vegas_render_width()` is the width Vegas would like, and it narrows
|
||||
`display_manager` to match while it asks. A `pause` plugin draws the
|
||||
whole panel in `display()`.
|
||||
|
||||
**Color Mode:**
|
||||
- Use RGB color mode
|
||||
@@ -289,17 +302,10 @@ class WeatherPlugin(BasePlugin):
|
||||
def get_vegas_content(self):
|
||||
"""Return cached Vegas image"""
|
||||
return self.vegas_image
|
||||
|
||||
def get_vegas_content_type(self):
|
||||
return 'multi'
|
||||
|
||||
def get_vegas_display_mode(self):
|
||||
return 'scroll'
|
||||
|
||||
def get_supported_vegas_modes(self):
|
||||
return ['scroll', 'static']
|
||||
```
|
||||
|
||||
It scrolls, the default participation, so it declares nothing else.
|
||||
|
||||
### System Architecture
|
||||
|
||||
Vegas mode consists of four core components working together to provide smooth 125 FPS continuous scrolling:
|
||||
@@ -382,7 +388,8 @@ Vegas mode consists of four core components working together to provide smooth 1
|
||||
|
||||
**Responsibilities:**
|
||||
- Convert plugin content to scrollable images
|
||||
- Handle different Vegas display modes (SCROLL, FIXED, STATIC)
|
||||
- Fetch the content of `scroll` plugins (a `pause` plugin is drawn by
|
||||
its own `display()` when the scroll pauses; see StreamManager)
|
||||
- Manage fallback for plugins without Vegas support
|
||||
- Cache plugin content for performance
|
||||
|
||||
@@ -391,21 +398,16 @@ Vegas mode consists of four core components working together to provide smooth 1
|
||||
- Calls `get_vegas_content()` if available
|
||||
- Falls back to `display()` method if not
|
||||
|
||||
2. **Handle display mode:**
|
||||
- SCROLL: Returns image as-is for continuous scrolling
|
||||
- FIXED_SEGMENT: Creates fixed-width block (panel_count * display_width)
|
||||
- STATIC: Marks content for pause-when-visible behavior
|
||||
|
||||
3. **Content type handling:**
|
||||
- `multi`: Multiple segments (list of images)
|
||||
- `static`: Single static image
|
||||
- `none`: Skip this plugin in current cycle
|
||||
2. **Participation** is decided by the StreamManager, not here
|
||||
(`resolve_vegas_participation()` in
|
||||
[`base_plugin.py`](../src/plugin_system/base_plugin.py)): `exclude`
|
||||
plugins never reach the adapter, and `pause` plugins are not fetched.
|
||||
|
||||
**Fallback Behavior:**
|
||||
- If plugin doesn't implement Vegas methods:
|
||||
- Calls plugin's `display()` method
|
||||
- Captures rendered display as static image
|
||||
- Treats as fixed segment
|
||||
- Scrolls it by as one block
|
||||
- Ensures all plugins work in Vegas mode without explicit support
|
||||
|
||||
#### 4. RenderPipeline
|
||||
@@ -508,7 +510,7 @@ All components use thread-safe patterns:
|
||||
If a plugin doesn't implement Vegas methods:
|
||||
- System calls the plugin's `display()` method
|
||||
- Captures the rendered display as a static image
|
||||
- Treats it as a fixed segment
|
||||
- Scrolls it by as one block
|
||||
|
||||
This ensures all plugins work in Vegas mode, even without explicit support.
|
||||
|
||||
|
||||
+23
-10
@@ -2,11 +2,11 @@
|
||||
|
||||
Generated by `scripts/plugin_api_usage.py` — do not edit by hand; re-run it (see [How to re-run](#how-to-re-run)).
|
||||
|
||||
- Scanned: 2026-09-29, core 3.7.0
|
||||
- Scanned: 2026-09-30, core 3.7.0
|
||||
- Monorepo: [ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) (main @ 4327c2e4), 46 plugins
|
||||
- Third-party plugins: 8 with their own repo in `plugins.json` (f1-live, gif-player, pga-tour-leaderboard, plex-marquee, ledmatrix-dresden-departures, tidbyt-baseball-scoreboard, sleeper-fantasy, ledmatrix-nascar)
|
||||
|
||||
**35 deprecated methods: 35 unused, 0 still used, 0 need review.**
|
||||
**37 deprecated methods: 36 unused, 1 still used, 0 need review.**
|
||||
|
||||
Counted per plugin: a *call* is `<receiver>.method` on an object named like the owner (`cache_manager`, `display_manager`, `font_manager`, `plugin_manager`), or on `self`/`super()` in a subclass; an *override* is `def method` in a subclass of the owner. *Review* hits are `.method` on a receiver whose type the scan cannot tell. *Internal* hits sit inside another deprecated core method and go with it. *Unrelated* hits are a different class's own method with the same name (a name collision), and never block removal; neither do hits in test files.
|
||||
|
||||
@@ -46,11 +46,17 @@ Counted per plugin: a *call* is `<receiver>.method` on an object named like the
|
||||
| `FontManager.add_font` | 3.8.0 | — | — | — | unused — safe to remove in 3.8.0 |
|
||||
| `FontManager.remove_font` | 3.8.0 | — | — | — | unused — safe to remove in 3.8.0 |
|
||||
| `FontManager.validate_font` | 3.8.0 | — | — | — | unused — safe to remove in 3.8.0 |
|
||||
| `BasePlugin.get_supported_vegas_modes` | 3.9.0 | core tests (2 test reviews) | blackjack (1 call, 1 override); calendar (1 override); olympics (1 override) | — | still used by blackjack, calendar, olympics — keep or migrate first |
|
||||
| `BasePlugin.get_vegas_segment_width` | 3.9.0 | core tests (1 test review) | — | — | unused — safe to remove in 3.9.0 |
|
||||
| `PluginManager.get_enabled_plugins` | 3.8.0 | — | — | — | unused — safe to remove in 3.8.0 |
|
||||
|
||||
## Unused — safe to remove (35)
|
||||
## Unused — safe to remove (36)
|
||||
|
||||
`CacheManager.has_data_changed`, `CacheManager.update_cache`, `CacheManager.setup_persistent_cache`, `CacheManager.get_sport_live_interval`, `CacheManager.get_sport_key_from_cache_key`, `CacheManager.get_background_cached_data`, `CacheManager.is_background_data_available`, `CacheManager.record_cache_hit`, `CacheManager.record_cache_miss`, `CacheManager.record_fetch_time`, `CacheManager.get_cache_metrics`, `CacheManager.log_cache_metrics`, `CacheManager.get_memory_cache_stats`, `DisplayManager.draw_sun`, `DisplayManager.draw_cloud`, `DisplayManager.draw_rain`, `DisplayManager.draw_snow`, `DisplayManager.draw_weather_icon`, `DisplayManager.draw_text_with_icons`, `DisplayManager.get_scrolling_stats`, `FontManager.get_manager_fonts`, `FontManager.get_detected_fonts`, `FontManager.unregister_plugin_fonts`, `FontManager.get_plugin_fonts`, `FontManager.set_override`, `FontManager.remove_override`, `FontManager.get_overrides`, `FontManager.get_available_fonts`, `FontManager.get_size_tokens`, `FontManager.get_performance_stats`, `FontManager.get_font_catalog`, `FontManager.add_font`, `FontManager.remove_font`, `FontManager.validate_font`, `PluginManager.get_enabled_plugins`
|
||||
`CacheManager.has_data_changed`, `CacheManager.update_cache`, `CacheManager.setup_persistent_cache`, `CacheManager.get_sport_live_interval`, `CacheManager.get_sport_key_from_cache_key`, `CacheManager.get_background_cached_data`, `CacheManager.is_background_data_available`, `CacheManager.record_cache_hit`, `CacheManager.record_cache_miss`, `CacheManager.record_fetch_time`, `CacheManager.get_cache_metrics`, `CacheManager.log_cache_metrics`, `CacheManager.get_memory_cache_stats`, `DisplayManager.draw_sun`, `DisplayManager.draw_cloud`, `DisplayManager.draw_rain`, `DisplayManager.draw_snow`, `DisplayManager.draw_weather_icon`, `DisplayManager.draw_text_with_icons`, `DisplayManager.get_scrolling_stats`, `FontManager.get_manager_fonts`, `FontManager.get_detected_fonts`, `FontManager.unregister_plugin_fonts`, `FontManager.get_plugin_fonts`, `FontManager.set_override`, `FontManager.remove_override`, `FontManager.get_overrides`, `FontManager.get_available_fonts`, `FontManager.get_size_tokens`, `FontManager.get_performance_stats`, `FontManager.get_font_catalog`, `FontManager.add_font`, `FontManager.remove_font`, `FontManager.validate_font`, `BasePlugin.get_vegas_segment_width`, `PluginManager.get_enabled_plugins`
|
||||
|
||||
## Still used — keep or migrate first (1)
|
||||
|
||||
`BasePlugin.get_supported_vegas_modes`
|
||||
|
||||
## Every hit
|
||||
|
||||
@@ -87,21 +93,28 @@ File paths are relative to the plugin's directory (core: the repo root).
|
||||
| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1635 | unrelated | `WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size)` |
|
||||
| `DisplayManager.draw_weather_icon` | ledmatrix-weather | weather_icons.py:168 | unrelated | `def draw_weather_icon(image: Image.Image, icon_code: str, x: int, y: int, size: int = DEFAULT_SIZE):` |
|
||||
| `DisplayManager.draw_text_with_icons` | core | src/plugin_system/testing/visual_display_manager.py:526 | unrelated | `def draw_text_with_icons(self, text: str, icons: List[tuple] = None,` |
|
||||
| `FontManager.get_font_catalog` | core tests | test/test_deprecation.py:217 | test call | `assert fm.get_font_catalog() == fm.font_catalog` |
|
||||
| `FontManager.get_font_catalog` | core tests | test/test_deprecation.py:229 | test call | `assert fm.get_font_catalog() == fm.font_catalog` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | core tests | test/test_vegas_participation.py:356 | test review | `assert plugin.get_supported_vegas_modes() == [` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | core tests | test/test_vegas_participation.py:358 | test review | `assert plugin.get_supported_vegas_modes()` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | blackjack | manager.py:732 | override | `def get_supported_vegas_modes(self):` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | blackjack | manager.py:695 | call | `if mode in self.get_supported_vegas_modes():` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | calendar | manager.py:875 | override | `def get_supported_vegas_modes(self) -> List[VegasDisplayMode]:` |
|
||||
| `BasePlugin.get_supported_vegas_modes` | olympics | manager.py:624 | override | `def get_supported_vegas_modes(self) -> List[VegasDisplayMode]:` |
|
||||
| `BasePlugin.get_vegas_segment_width` | core tests | test/test_vegas_participation.py:359 | test review | `assert plugin.get_vegas_segment_width() == 2` |
|
||||
|
||||
## Sources scanned
|
||||
|
||||
| Source | Group | Python files | Hits |
|
||||
|---|---|---|---|
|
||||
| core | core | 158 | 20 |
|
||||
| core tests | core-tests | 316 | 14 |
|
||||
| core | core | 164 | 20 |
|
||||
| core tests | core-tests | 323 | 17 |
|
||||
| 7-segment-clock | monorepo | 3 | 0 |
|
||||
| afl-scoreboard | monorepo | 34 | 0 |
|
||||
| baseball-scoreboard | monorepo | 60 | 0 |
|
||||
| basketball-scoreboard | monorepo | 48 | 0 |
|
||||
| birdnet-go | monorepo | 2 | 0 |
|
||||
| blackjack | monorepo | 7 | 0 |
|
||||
| calendar | monorepo | 5 | 0 |
|
||||
| blackjack | monorepo | 7 | 2 |
|
||||
| calendar | monorepo | 5 | 1 |
|
||||
| christmas-countdown | monorepo | 3 | 0 |
|
||||
| clock-simple | monorepo | 2 | 0 |
|
||||
| countdown | monorepo | 5 | 0 |
|
||||
@@ -130,7 +143,7 @@ File paths are relative to the plugin's directory (core: the repo root).
|
||||
| nrl-scoreboard | monorepo | 29 | 0 |
|
||||
| odds-ticker | monorepo | 9 | 0 |
|
||||
| of-the-day | monorepo | 14 | 0 |
|
||||
| olympics | monorepo | 16 | 0 |
|
||||
| olympics | monorepo | 16 | 1 |
|
||||
| on-air | monorepo | 2 | 0 |
|
||||
| pomodoro-timer | monorepo | 3 | 0 |
|
||||
| soccer-scoreboard | monorepo | 46 | 0 |
|
||||
|
||||
@@ -314,6 +314,58 @@ rotating one at a time. Plugins control how their content appears via
|
||||
these hooks. See [ADVANCED_FEATURES.md](ADVANCED_FEATURES.md) for the user
|
||||
side of Vegas mode.
|
||||
|
||||
#### Vegas participation
|
||||
|
||||
Each plugin takes part in Vegas mode in one of three ways:
|
||||
|
||||
| Participation | What Vegas does |
|
||||
|---|---|
|
||||
| `'scroll'` | The plugin's content (`get_vegas_content()`) scrolls by with everything else |
|
||||
| `'pause'` | The scroll stops when the plugin's turn comes round; its `display()` draws it full screen for `get_display_duration()` seconds, then the scroll resumes |
|
||||
| `'exclude'` | The plugin is left out of Vegas mode |
|
||||
|
||||
Declare the plugin's default in `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-alerts",
|
||||
"vegas_participation": "pause"
|
||||
}
|
||||
```
|
||||
|
||||
The user can override it per plugin with `vegas_participation` in that
|
||||
plugin's config section (it is one of the core-owned properties, see
|
||||
[PLUGIN_CONFIG_CORE_PROPERTIES.md](PLUGIN_CONFIG_CORE_PROPERTIES.md)).
|
||||
Vegas resolves it in this order:
|
||||
|
||||
1. the user's `vegas_participation` config value;
|
||||
2. the plugin's `get_vegas_participation()` — the default implementation
|
||||
reads the manifest's `vegas_participation`, then derives a value from
|
||||
the legacy hooks below;
|
||||
3. derived from the legacy hooks: `get_vegas_display_mode()` returning
|
||||
`VegasDisplayMode.STATIC` → `'pause'`; otherwise
|
||||
`get_vegas_content_type()` returning `'none'` → `'exclude'`; everything
|
||||
else → `'scroll'`.
|
||||
|
||||
Step 3 is exactly what Vegas did before participation existed, so a plugin
|
||||
that declares nothing behaves as it always has. Manifest
|
||||
`vegas_participation` is new in core 3.8.0; older cores ignore it and use
|
||||
the legacy hooks.
|
||||
|
||||
#### `get_vegas_participation() -> str`
|
||||
|
||||
Returns `'scroll'`, `'pause'` or `'exclude'`. Override it only when the
|
||||
answer depends on state — pause only while an alert is live, exclude while
|
||||
there is nothing to show; for a fixed answer use the manifest. Vegas applies
|
||||
the user's config value before calling an override, so an override does not
|
||||
need to check it. A value that is not one of the three is ignored with a log
|
||||
line and the legacy hooks decide.
|
||||
|
||||
```python
|
||||
def get_vegas_participation(self):
|
||||
return 'pause' if self._alert_is_live() else 'scroll'
|
||||
```
|
||||
|
||||
#### `get_vegas_content() -> Optional[PIL.Image | List[PIL.Image] | None]`
|
||||
|
||||
Return content to inject into the scroll. Multi-item plugins (sports,
|
||||
@@ -321,26 +373,40 @@ odds, news) should return a *list* of PIL Images so each item scrolls
|
||||
independently. Static plugins (clock, weather) can return a single image.
|
||||
Returning `None` falls back to capturing whatever `display()` produces.
|
||||
|
||||
#### `get_vegas_content_type() -> str`
|
||||
#### `get_vegas_render_width() -> int`
|
||||
|
||||
`'multi'`, `'static'`, or `'none'`. Affects how Vegas mode treats the
|
||||
plugin. Default `'static'`.
|
||||
The width Vegas wants this plugin's content to occupy, from the plugin's
|
||||
`vegas_width_pct` config value or the global
|
||||
`display.vegas_scroll.render_width_pct`. Vegas also narrows
|
||||
`display_manager` while it asks for content, so a plugin that sizes itself
|
||||
from `display_manager.width` does not need to read this.
|
||||
|
||||
#### `get_vegas_display_mode() -> VegasDisplayMode`
|
||||
#### Legacy: `get_vegas_content_type()` and `get_vegas_display_mode()`
|
||||
|
||||
Returns one of `VegasDisplayMode.SCROLL`, `FIXED_SEGMENT`, or `STATIC`.
|
||||
Read from `config["vegas_mode"]` or override directly.
|
||||
Superseded by participation, and still read to derive it when neither the
|
||||
user nor the manifest declares one (step 3 above). Only two answers ever
|
||||
mattered: `get_vegas_content_type()` returning `'none'`, and
|
||||
`get_vegas_display_mode()` returning `VegasDisplayMode.STATIC`.
|
||||
|
||||
#### `get_supported_vegas_modes() -> List[VegasDisplayMode]`
|
||||
- `get_vegas_content_type()` returns `'multi'`, `'static'` or `'none'`
|
||||
(default `'static'`).
|
||||
- `get_vegas_display_mode()` returns a `VegasDisplayMode` member (not a
|
||||
string — the string `'static'` never paused anything). The default reads
|
||||
the plugin's `vegas_mode` config value (`"scroll"`, `"fixed"` or
|
||||
`"static"`), else maps content type `'multi'` to `SCROLL` and anything
|
||||
else to `FIXED_SEGMENT`.
|
||||
|
||||
The set of Vegas modes this plugin can render. Used by the UI to populate
|
||||
the mode selector for this plugin.
|
||||
`SCROLL` and `FIXED_SEGMENT` (and `vegas_mode` `"scroll"` and `"fixed"`)
|
||||
have always behaved identically: both scroll. The distinction is deprecated
|
||||
and goes away in 3.9.0 — see [Deprecated APIs](#deprecated-apis).
|
||||
|
||||
#### `get_vegas_segment_width() -> Optional[int]`
|
||||
#### Deprecated: `get_supported_vegas_modes()` and `get_vegas_segment_width()`
|
||||
|
||||
For `FIXED_SEGMENT` plugins, the number of *panels* the segment
|
||||
occupies in the scroll (pixel width = panels × `single_panel_width`,
|
||||
from `display.hardware.cols`). `None` uses the default of 1 panel.
|
||||
Never read by core, and removed in 3.9.0: calling the `BasePlugin`
|
||||
implementation logs a deprecation warning. A plugin's own override keeps
|
||||
working for the plugin itself. `get_vegas_segment_width()` read the
|
||||
`vegas_panel_count` config value, which has never affected Vegas — a card's
|
||||
width comes from `get_vegas_content()` and `vegas_width_pct`.
|
||||
|
||||
> The full source for `BasePlugin` lives in
|
||||
> `src/plugin_system/base_plugin.py`. If a method here disagrees with the
|
||||
@@ -1095,3 +1161,17 @@ are removed in 3.8.0; the rest stay until their callers migrate.
|
||||
| `font_manager` | `set_override`, `remove_override`, `get_overrides`, `add_font`, `remove_font`, `validate_font`, `get_size_tokens`, `get_performance_stats`, `get_manager_fonts`, `get_detected_fonts`, `get_plugin_fonts`, `unregister_plugin_fonts` | no replacement |
|
||||
| `plugin_manager` | `get_enabled_plugins` | check `enabled` on the entries in `plugin_manager.plugins` |
|
||||
|
||||
### Removed in 3.9.0
|
||||
|
||||
The Vegas APIs that described a fixed-width segment, which Vegas never
|
||||
implemented. Vegas participation (`'scroll'`, `'pause'`, `'exclude'`, see
|
||||
[Vegas scroll hooks](#vegas-scroll-hooks)) replaces them. Calling one of the
|
||||
methods, or setting `vegas_panel_count`, logs a warning once per process.
|
||||
No official plugin calls them; calendar, olympics and blackjack override
|
||||
`get_supported_vegas_modes()`, which keeps working for the plugin itself.
|
||||
|
||||
| What | Instead |
|
||||
|---|---|
|
||||
| `BasePlugin.get_supported_vegas_modes()` | declare `vegas_participation` in the manifest |
|
||||
| `BasePlugin.get_vegas_segment_width()` and the `vegas_panel_count` config key | nothing: a card's width comes from `get_vegas_content()` and `vegas_width_pct` |
|
||||
| `VegasDisplayMode.SCROLL` vs `FIXED_SEGMENT` (`vegas_mode` `"scroll"` vs `"fixed"`) | `'scroll'` participation; the two always behaved the same |
|
||||
|
||||
@@ -33,6 +33,20 @@ is `CORE_PLUGIN_PROPERTIES` in `src/plugin_system/schema_manager.py`):
|
||||
- Read by `src/vegas_mode/plugin_adapter.py` and `BasePlugin`, which
|
||||
validate the values themselves and ignore a bad one with a log line
|
||||
|
||||
5. **`vegas_participation`** (string enum: `"scroll"`, `"pause"`,
|
||||
`"exclude"`; no default)
|
||||
- Description: how this plugin takes part in Vegas mode — its content
|
||||
scrolls by, the scroll pauses for its turn and shows it full screen, or
|
||||
it is left out
|
||||
- Overrides the plugin's own default (its manifest's
|
||||
`vegas_participation`, else what its legacy Vegas hooks say); unset
|
||||
means "use the plugin's default"
|
||||
- Deliberately has no default: one would be written into every plugin's
|
||||
config and override what each plugin declares
|
||||
- Read by `resolve_vegas_participation()` in
|
||||
`src/plugin_system/base_plugin.py`; see
|
||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-participation)
|
||||
|
||||
`skin` and `skin_options` were core properties until the skin system was
|
||||
removed. A plugin config saved with them still loads and saves; the keys are
|
||||
dropped on the next save (see `RETIRED_PLUGIN_KEYS` in `schema_manager.py`).
|
||||
|
||||
@@ -511,13 +511,21 @@ List all installed plugins with their status and metadata.
|
||||
"branch": "main",
|
||||
"web_ui_actions": [],
|
||||
"vegas_mode": null,
|
||||
"vegas_content_type": null
|
||||
"vegas_content_type": null,
|
||||
"vegas_participation": "scroll"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`vegas_participation` is what Vegas mode does with the plugin: `"scroll"`,
|
||||
`"pause"` or `"exclude"` (see
|
||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-participation)).
|
||||
For a plugin that is not loaded it is only the user's own
|
||||
`vegas_participation` setting, or `null`. `vegas_mode` and
|
||||
`vegas_content_type` are the legacy hooks' raw answers.
|
||||
|
||||
### Get Plugin Configuration
|
||||
|
||||
**GET** `/api/v3/plugins/config?plugin_id=<plugin_id>`
|
||||
|
||||
@@ -149,6 +149,11 @@
|
||||
},
|
||||
"description": "Array of display mode names this plugin provides"
|
||||
},
|
||||
"vegas_participation": {
|
||||
"type": "string",
|
||||
"enum": ["scroll", "pause", "exclude"],
|
||||
"description": "How this plugin takes part in Vegas mode by default: 'scroll' (its content scrolls by), 'pause' (the scroll stops for its turn and display() draws it full screen) or 'exclude' (left out). A user's per-plugin vegas_participation setting overrides it. Omit it to derive the participation from get_vegas_display_mode() / get_vegas_content_type(). Cores before 3.8.0 ignore it."
|
||||
},
|
||||
"api_requirements": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -53,3 +53,27 @@ def deprecated(removal: str, alternative: Optional[str] = None) -> Callable[[F],
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def warn_deprecated(what: str, removal: str, alternative: Optional[str] = None,
|
||||
once_key: Optional[str] = None) -> bool:
|
||||
"""Warn that ``what`` will be removed in ``removal``, once per process.
|
||||
|
||||
For what ``@deprecated`` cannot decorate: a config key, a manifest field,
|
||||
a value a hook returns. Same message, log line and DeprecationWarning as
|
||||
the decorator. ``once_key`` (default: ``what``) is what "once" counts
|
||||
against, so one deprecated key can warn once for each plugin that sets it.
|
||||
|
||||
Returns whether this call warned.
|
||||
"""
|
||||
message = f"{what} is deprecated and will be removed in LEDMatrix {removal}"
|
||||
if alternative:
|
||||
message += f"; {alternative}"
|
||||
key = once_key or what
|
||||
with _warned_lock:
|
||||
if key in _warned:
|
||||
return False
|
||||
_warned.add(key)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
||||
return True
|
||||
|
||||
@@ -13,6 +13,7 @@ from enum import Enum
|
||||
from typing import Dict, Any, Optional, List
|
||||
import os
|
||||
import sys
|
||||
from src.deprecation import deprecated, warn_deprecated
|
||||
from src.logging_config import get_logger
|
||||
|
||||
|
||||
@@ -65,27 +66,178 @@ def _fallback_font_manager() -> Any:
|
||||
|
||||
class VegasDisplayMode(Enum):
|
||||
"""
|
||||
Display mode for Vegas scroll integration.
|
||||
Legacy display mode for Vegas scroll integration.
|
||||
|
||||
Determines how a plugin's content behaves within the continuous scroll:
|
||||
Superseded by :meth:`BasePlugin.get_vegas_participation`. Vegas still
|
||||
reads a plugin's :meth:`BasePlugin.get_vegas_display_mode` to derive its
|
||||
participation when nothing declares one, and only STATIC matters there:
|
||||
|
||||
- SCROLL: Content scrolls continuously within the stream.
|
||||
Best for multi-item plugins like sports scores, odds tickers, news feeds.
|
||||
Plugin provides multiple frames via get_vegas_content().
|
||||
|
||||
- FIXED_SEGMENT: Content is a fixed-width block that scrolls BY with
|
||||
the rest of the content. Best for static info like clock, weather.
|
||||
Plugin provides a single image sized to vegas_panel_count panels.
|
||||
|
||||
- STATIC: Scroll pauses, plugin displays for its duration, then scroll
|
||||
resumes. Best for important alerts or detailed views that need attention.
|
||||
Plugin uses standard display() method during the pause.
|
||||
- STATIC: the scroll pauses for the plugin's turn and its display() draws
|
||||
it full screen -- participation ``'pause'``.
|
||||
- SCROLL and FIXED_SEGMENT: the plugin's content joins the scroll --
|
||||
participation ``'scroll'``. Vegas has never told the two apart: a card's
|
||||
width comes from get_vegas_content() and ``vegas_width_pct``, not from
|
||||
the mode. The distinction is deprecated and goes away in LEDMatrix 3.9.0.
|
||||
"""
|
||||
SCROLL = "scroll"
|
||||
FIXED_SEGMENT = "fixed"
|
||||
STATIC = "static"
|
||||
|
||||
|
||||
#: How a plugin takes part in Vegas mode (BasePlugin.get_vegas_participation):
|
||||
#:
|
||||
#: - ``'scroll'``: its content joins the scrolling strip.
|
||||
#: - ``'pause'``: the scroll stops when the plugin's turn comes round, and its
|
||||
#: display() draws it full screen for its display duration.
|
||||
#: - ``'exclude'``: it is left out of Vegas mode.
|
||||
VEGAS_PARTICIPATION_VALUES = ('scroll', 'pause', 'exclude')
|
||||
|
||||
#: The release that removes get_supported_vegas_modes(),
|
||||
#: get_vegas_segment_width(), the ``vegas_panel_count`` setting and the
|
||||
#: SCROLL / FIXED_SEGMENT distinction. The two @deprecated markers below
|
||||
#: spell it as a literal, because tools that read markers statically (the
|
||||
#: deprecation tests, the plugin API usage scan) cannot follow a name.
|
||||
VEGAS_LEGACY_REMOVAL = "3.9.0"
|
||||
|
||||
_vegas_logger = get_logger(__name__)
|
||||
_vegas_warned: set = set()
|
||||
|
||||
|
||||
def _vegas_warn_once(key: Any, message: str, *args: Any) -> None:
|
||||
"""Log a warning about a plugin's Vegas settings once per process.
|
||||
|
||||
Participation is resolved at every rotation refresh, so a bad value would
|
||||
otherwise log on every one of them.
|
||||
"""
|
||||
if key in _vegas_warned:
|
||||
return
|
||||
_vegas_warned.add(key)
|
||||
_vegas_logger.warning(message, *args)
|
||||
|
||||
|
||||
def vegas_participation_value(value: Any) -> Optional[str]:
|
||||
"""``value`` as one of VEGAS_PARTICIPATION_VALUES, or None if it is not one.
|
||||
|
||||
Case and surrounding whitespace are ignored; anything that is not a string
|
||||
(None, a MagicMock standing in for a plugin in a test) is not a value.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.strip().lower()
|
||||
if value in VEGAS_PARTICIPATION_VALUES:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def configured_vegas_participation(plugin_id: str, config: Any) -> Optional[str]:
|
||||
"""The user's ``vegas_participation`` setting in a plugin's config, if valid.
|
||||
|
||||
An unset or empty value is no setting. Anything else that is not a
|
||||
participation is logged once and ignored, so the plugin keeps its own.
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
raw = config.get('vegas_participation')
|
||||
if raw is None or (isinstance(raw, str) and not raw.strip()):
|
||||
return None
|
||||
value = vegas_participation_value(raw)
|
||||
if value is None:
|
||||
_vegas_warn_once(
|
||||
('config', plugin_id, repr(raw)),
|
||||
"[%s] Invalid vegas_participation %r, expected one of %s; ignoring it",
|
||||
plugin_id, raw, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return value
|
||||
|
||||
|
||||
def legacy_vegas_participation(plugin: Any) -> str:
|
||||
"""The participation a plugin's pre-3.8 Vegas hooks describe.
|
||||
|
||||
Exactly what Vegas decided from them before participation existed:
|
||||
|
||||
1. get_vegas_display_mode() returning ``VegasDisplayMode.STATIC`` pauses,
|
||||
whatever the content type -- a STATIC plugin whose content type is
|
||||
``'none'`` was still kept in the rotation to pause it.
|
||||
2. Otherwise get_vegas_content_type() returning ``'none'`` excludes.
|
||||
3. Everything else scrolls. SCROLL and FIXED_SEGMENT were never told
|
||||
apart, and neither were content types ``'multi'``, ``'static'`` or any
|
||||
other string.
|
||||
|
||||
Only the enum member counts as STATIC (a plugin returning the string
|
||||
``'static'`` never paused), and a hook that raises or is missing counts as
|
||||
not STATIC and as content type ``'static'``.
|
||||
"""
|
||||
display_mode = None
|
||||
get_mode = getattr(plugin, 'get_vegas_display_mode', None)
|
||||
if get_mode is not None:
|
||||
try:
|
||||
display_mode = get_mode()
|
||||
except Exception:
|
||||
_vegas_logger.debug("get_vegas_display_mode() failed on %s; not pausing",
|
||||
type(plugin).__name__, exc_info=True)
|
||||
if display_mode == VegasDisplayMode.STATIC:
|
||||
return 'pause'
|
||||
|
||||
content_type = 'static'
|
||||
get_type = getattr(plugin, 'get_vegas_content_type', None)
|
||||
if get_type is not None:
|
||||
try:
|
||||
content_type = get_type()
|
||||
except Exception:
|
||||
_vegas_logger.debug("get_vegas_content_type() failed on %s; treating as 'static'",
|
||||
type(plugin).__name__, exc_info=True)
|
||||
if content_type == 'none':
|
||||
return 'exclude'
|
||||
return 'scroll'
|
||||
|
||||
|
||||
def resolve_vegas_participation(plugin: Any, plugin_id: Optional[str] = None) -> str:
|
||||
"""How Vegas mode treats ``plugin``: ``'scroll'``, ``'pause'`` or ``'exclude'``.
|
||||
|
||||
What the core calls, rather than the plugin's own
|
||||
get_vegas_participation(), so the user's setting wins even over a plugin
|
||||
that overrides that method, and so a plugin that is not a BasePlugin (or a
|
||||
test double) still gets the legacy derivation:
|
||||
|
||||
1. the user's ``vegas_participation`` in the plugin's config;
|
||||
2. the plugin's get_vegas_participation(), when it returns a valid value
|
||||
(BasePlugin's reads the manifest's ``vegas_participation``, then
|
||||
derives one from the legacy hooks);
|
||||
3. legacy_vegas_participation().
|
||||
|
||||
Also where the deprecated ``vegas_panel_count`` setting is reported, once
|
||||
per plugin. Never raises.
|
||||
"""
|
||||
pid = plugin_id or getattr(plugin, 'plugin_id', None) or type(plugin).__name__
|
||||
config = getattr(plugin, 'config', None)
|
||||
if isinstance(config, dict) and 'vegas_panel_count' in config:
|
||||
warn_deprecated(
|
||||
f"The vegas_panel_count setting (plugin '{pid}')", VEGAS_LEGACY_REMOVAL,
|
||||
"it has no effect -- use vegas_width_pct to size the plugin's card",
|
||||
once_key=f"vegas_panel_count:{pid}")
|
||||
|
||||
configured = configured_vegas_participation(pid, config)
|
||||
if configured is not None:
|
||||
return configured
|
||||
|
||||
getter = getattr(plugin, 'get_vegas_participation', None)
|
||||
if callable(getter):
|
||||
try:
|
||||
declared = getter()
|
||||
except Exception:
|
||||
_vegas_logger.exception("[%s] get_vegas_participation() failed; "
|
||||
"using its legacy Vegas hooks", pid)
|
||||
declared = None
|
||||
value = vegas_participation_value(declared)
|
||||
if value is not None:
|
||||
return value
|
||||
if isinstance(declared, str):
|
||||
_vegas_warn_once(
|
||||
('declared', pid, declared),
|
||||
"[%s] get_vegas_participation() returned %r, expected one of %s; "
|
||||
"using its legacy Vegas hooks",
|
||||
pid, declared, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return legacy_vegas_participation(plugin)
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""
|
||||
Base class that all plugins must inherit from.
|
||||
@@ -834,41 +986,97 @@ class BasePlugin(ABC):
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_vegas_participation(self) -> str:
|
||||
"""
|
||||
How this plugin takes part in Vegas mode: ``'scroll'``, ``'pause'`` or
|
||||
``'exclude'``.
|
||||
|
||||
- ``'scroll'``: the plugin's content (get_vegas_content()) joins the
|
||||
scrolling strip.
|
||||
- ``'pause'``: the scroll stops when the plugin's turn comes round, and
|
||||
its display() draws it full screen for get_display_duration().
|
||||
- ``'exclude'``: the plugin is left out of Vegas mode.
|
||||
|
||||
Resolved in this order:
|
||||
|
||||
1. the user's ``vegas_participation`` setting in this plugin's config
|
||||
(the web UI's per-plugin override);
|
||||
2. ``vegas_participation`` in the plugin's manifest.json -- the way a
|
||||
plugin declares its own default;
|
||||
3. derived from the legacy hooks, so a plugin written before this
|
||||
method existed keeps the behaviour it had: get_vegas_display_mode()
|
||||
returning ``VegasDisplayMode.STATIC`` pauses, otherwise
|
||||
get_vegas_content_type() returning ``'none'`` excludes, and
|
||||
everything else scrolls.
|
||||
|
||||
Declare a fixed participation in the manifest rather than overriding
|
||||
this. Override it only when the answer depends on state -- pause only
|
||||
while an alert is live, exclude while there is nothing to show. Vegas
|
||||
applies the user's setting before calling an override, so an override
|
||||
need not check it.
|
||||
|
||||
Returns:
|
||||
One of VEGAS_PARTICIPATION_VALUES.
|
||||
|
||||
Example:
|
||||
def get_vegas_participation(self):
|
||||
return 'pause' if self._alert_is_live() else 'scroll'
|
||||
"""
|
||||
configured = configured_vegas_participation(self.plugin_id, self.config)
|
||||
if configured is not None:
|
||||
return configured
|
||||
manifest_default = self._manifest_vegas_participation()
|
||||
if manifest_default is not None:
|
||||
return manifest_default
|
||||
return legacy_vegas_participation(self)
|
||||
|
||||
def _manifest_vegas_participation(self) -> Optional[str]:
|
||||
"""``vegas_participation`` from this plugin's manifest, if valid."""
|
||||
manifests = getattr(self.plugin_manager, 'plugin_manifests', None)
|
||||
manifest = manifests.get(self.plugin_id) if isinstance(manifests, dict) else None
|
||||
if not isinstance(manifest, dict) or manifest.get('vegas_participation') is None:
|
||||
return None
|
||||
raw = manifest['vegas_participation']
|
||||
value = vegas_participation_value(raw)
|
||||
if value is None:
|
||||
_vegas_warn_once(
|
||||
('manifest', self.plugin_id, repr(raw)),
|
||||
"[%s] manifest vegas_participation %r is not one of %s; ignoring it",
|
||||
self.plugin_id, raw, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return value
|
||||
|
||||
def get_vegas_content_type(self) -> str:
|
||||
"""
|
||||
Indicate the type of content this plugin provides for Vegas scroll.
|
||||
Legacy: the type of content this plugin provides for Vegas scroll.
|
||||
|
||||
Override this to specify how Vegas mode should treat this plugin's content.
|
||||
Superseded by get_vegas_participation(). Vegas reads it only to derive
|
||||
a participation when neither the user nor the manifest declares one,
|
||||
and only ``'none'`` matters there: it excludes the plugin (unless
|
||||
get_vegas_display_mode() says STATIC). Every other value scrolls.
|
||||
|
||||
Returns:
|
||||
'multi' - Plugin has multiple scrollable items (sports, odds, news)
|
||||
'static' - Plugin is a static block (clock, weather, music)
|
||||
'none' - Plugin should not appear in Vegas scroll mode
|
||||
|
||||
Example:
|
||||
def get_vegas_content_type(self):
|
||||
return 'multi' # We have multiple games to scroll
|
||||
"""
|
||||
return 'static'
|
||||
|
||||
def get_vegas_display_mode(self) -> VegasDisplayMode:
|
||||
"""
|
||||
Get the display mode for Vegas scroll integration.
|
||||
Legacy: the display mode for Vegas scroll integration.
|
||||
|
||||
This method determines how the plugin's content behaves within Vegas mode:
|
||||
- SCROLL: Content scrolls continuously (multi-item plugins)
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by (clock, weather)
|
||||
- STATIC: Pause scroll to display (alerts, detailed views)
|
||||
Superseded by get_vegas_participation(). Vegas reads it only to derive
|
||||
a participation when neither the user nor the manifest declares one,
|
||||
and only STATIC matters there: it pauses the scroll for the plugin's
|
||||
turn. SCROLL and FIXED_SEGMENT both scroll -- Vegas has never told them
|
||||
apart, and the distinction is deprecated (removed in 3.9.0).
|
||||
|
||||
Override to change default behavior. By default, reads from config
|
||||
or maps legacy get_vegas_content_type() for backward compatibility.
|
||||
Reads the plugin's ``vegas_mode`` config value, else maps
|
||||
get_vegas_content_type() ('multi' to SCROLL, anything else to
|
||||
FIXED_SEGMENT).
|
||||
|
||||
Returns:
|
||||
VegasDisplayMode enum value
|
||||
|
||||
Example:
|
||||
def get_vegas_display_mode(self):
|
||||
return VegasDisplayMode.SCROLL
|
||||
"""
|
||||
# Check for explicit config setting first
|
||||
config_mode = self.config.get("vegas_mode")
|
||||
@@ -888,13 +1096,16 @@ class BasePlugin(ABC):
|
||||
return VegasDisplayMode.SCROLL
|
||||
return VegasDisplayMode.FIXED_SEGMENT
|
||||
|
||||
@deprecated("3.9.0",
|
||||
"nothing reads it -- declare vegas_participation in the manifest instead")
|
||||
def get_supported_vegas_modes(self) -> List[VegasDisplayMode]:
|
||||
"""
|
||||
Return list of Vegas display modes this plugin supports.
|
||||
Deprecated: the Vegas display modes this plugin supports.
|
||||
|
||||
Not currently consulted by core: neither Vegas mode nor the web UI
|
||||
calls it. It is kept, and plugins override it, as the declared set of
|
||||
modes a future mode picker would offer.
|
||||
Never consulted by core -- neither Vegas mode nor the web UI calls it
|
||||
-- and removed in LEDMatrix 3.9.0. A plugin's own override keeps
|
||||
working for the plugin itself; calling this base implementation logs a
|
||||
deprecation warning.
|
||||
|
||||
By default:
|
||||
- 'multi' content type plugins support SCROLL and FIXED_SEGMENT
|
||||
@@ -903,11 +1114,6 @@ class BasePlugin(ABC):
|
||||
|
||||
Returns:
|
||||
List of VegasDisplayMode values this plugin can use
|
||||
|
||||
Example:
|
||||
def get_supported_vegas_modes(self):
|
||||
# This plugin only makes sense as a scrolling ticker
|
||||
return [VegasDisplayMode.SCROLL]
|
||||
"""
|
||||
content_type = self.get_vegas_content_type()
|
||||
|
||||
@@ -918,30 +1124,21 @@ class BasePlugin(ABC):
|
||||
else: # 'static'
|
||||
return [VegasDisplayMode.FIXED_SEGMENT, VegasDisplayMode.STATIC]
|
||||
|
||||
@deprecated("3.9.0",
|
||||
"nothing reads it -- Vegas sizes a card from vegas_width_pct "
|
||||
"(see get_vegas_render_width())")
|
||||
def get_vegas_segment_width(self) -> Optional[int]:
|
||||
"""
|
||||
Get the preferred width for this plugin in Vegas FIXED_SEGMENT mode.
|
||||
Deprecated: the number of panels this plugin wanted as a FIXED_SEGMENT.
|
||||
|
||||
Not currently consulted by core: Vegas mode sizes a card from the
|
||||
``vegas_width_pct`` / ``vegas_scroll.render_width_pct`` settings
|
||||
(see get_vegas_render_width()). Kept because plugins override it.
|
||||
|
||||
Returns the number of panels this plugin should occupy when displayed
|
||||
as a fixed segment. The actual pixel width is calculated as:
|
||||
width = panels * single_panel_width
|
||||
|
||||
Where single_panel_width comes from display.hardware.cols in config.
|
||||
|
||||
Override to provide dynamic sizing based on content.
|
||||
Returns None to use the default (1 panel).
|
||||
Never consulted by core: Vegas sizes a card from the
|
||||
``vegas_width_pct`` / ``vegas_scroll.render_width_pct`` settings (see
|
||||
get_vegas_render_width()). Removed, with the ``vegas_panel_count``
|
||||
setting it reads, in LEDMatrix 3.9.0.
|
||||
|
||||
Returns:
|
||||
Number of panels, or None for default (1 panel)
|
||||
|
||||
Example:
|
||||
def get_vegas_segment_width(self):
|
||||
# Clock needs 2 panels to show time clearly
|
||||
return 2
|
||||
``vegas_panel_count`` from config when it is a positive integer,
|
||||
else None
|
||||
"""
|
||||
raw_value = self.config.get("vegas_panel_count", None)
|
||||
if raw_value is None:
|
||||
|
||||
@@ -569,7 +569,8 @@ class PluginManager:
|
||||
#: prefix rule would silently stop validating it.
|
||||
#:
|
||||
#: Read by: ``vegas_mode/plugin_adapter.py`` (``vegas_width_pct``,
|
||||
#: ``vegas_overflow``) and ``base_plugin.py`` (``vegas_max_width_screens``).
|
||||
#: ``vegas_overflow``) and ``base_plugin.py`` (``vegas_max_width_screens``,
|
||||
#: ``vegas_participation``).
|
||||
#:
|
||||
#: The list itself lives with the other core-owned per-plugin properties in
|
||||
#: ``schema_manager.CORE_PLUGIN_PROPERTIES``, which the web save path also
|
||||
|
||||
@@ -127,8 +127,9 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
"description": "Enable live priority takeover when plugin has live content"
|
||||
},
|
||||
# Vegas tuning read by vegas_mode/plugin_adapter.py and base_plugin.py.
|
||||
# Left untyped: the adapter validates them itself and ignores a bad
|
||||
# value with a log line, so a stored one must never block a save.
|
||||
# These three are left untyped: the adapter validates them itself and
|
||||
# ignores a bad value with a log line, so a stored one must never block a
|
||||
# save.
|
||||
"vegas_width_pct": {
|
||||
"description": "Vegas mode: width of this plugin's card, as a percentage of the panel"
|
||||
},
|
||||
@@ -138,6 +139,22 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
"vegas_max_width_screens": {
|
||||
"description": "Vegas mode: widest this plugin's card may be, in screens"
|
||||
},
|
||||
# Read by resolve_vegas_participation / BasePlugin.get_vegas_participation.
|
||||
# An enum with no default: a default would be written into every plugin's
|
||||
# config and override the participation the plugin itself declares.
|
||||
"vegas_participation": {
|
||||
"type": "string",
|
||||
"enum": ["scroll", "pause", "exclude"],
|
||||
"title": "Vegas participation",
|
||||
"description": (
|
||||
"Vegas mode: how this plugin takes part in the scrolling ticker. "
|
||||
"'scroll' = its content scrolls by with everything else; "
|
||||
"'pause' = the ticker stops for this plugin's turn and shows it "
|
||||
"full screen for its display duration; "
|
||||
"'exclude' = leave it out of Vegas mode. "
|
||||
"Leave unset to use the plugin's own default."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
#: The keys of CORE_PLUGIN_PROPERTIES that are Vegas tuning rather than plugin
|
||||
@@ -145,6 +162,7 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
#: PluginManager.CORE_OWNED_CONFIG_KEYS).
|
||||
CORE_VEGAS_TUNING_KEYS = frozenset({
|
||||
'vegas_width_pct', 'vegas_overflow', 'vegas_max_width_screens',
|
||||
'vegas_participation',
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ Main orchestrator for Vegas-style continuous scroll mode. Coordinates between
|
||||
StreamManager, RenderPipeline, and the display system to provide smooth
|
||||
continuous scrolling of all enabled plugin content.
|
||||
|
||||
Supports three display modes per plugin:
|
||||
- SCROLL: Content scrolls continuously within the stream
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by with other content
|
||||
- STATIC: Scroll pauses, plugin displays for its duration, then resumes
|
||||
Each plugin takes part in one of three ways (its Vegas participation, see
|
||||
BasePlugin.get_vegas_participation):
|
||||
- 'scroll': its content scrolls by within the stream
|
||||
- 'pause': the scroll pauses, the plugin displays for its duration, then
|
||||
the scroll resumes
|
||||
- 'exclude': left out
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -1369,29 +1369,6 @@ class PluginAdapter:
|
||||
logger.debug("[%s] Cleared plugin scroll cache", plugin_id)
|
||||
return cleared
|
||||
|
||||
def get_content_type(self, plugin: 'BasePlugin', plugin_id: str) -> str:
|
||||
"""
|
||||
Get the type of content a plugin provides.
|
||||
|
||||
Args:
|
||||
plugin: Plugin instance
|
||||
plugin_id: Plugin identifier
|
||||
|
||||
Returns:
|
||||
'multi' for multiple items, 'static' for single frame, 'none' for excluded
|
||||
"""
|
||||
if hasattr(plugin, 'get_vegas_content_type'):
|
||||
try:
|
||||
return plugin.get_vegas_content_type()
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
logger.exception(
|
||||
"Error calling get_vegas_content_type() on %s",
|
||||
plugin_id
|
||||
)
|
||||
|
||||
# Default to static for plugins without explicit type
|
||||
return 'static'
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up resources."""
|
||||
with self._cache_lock:
|
||||
|
||||
@@ -5,23 +5,25 @@ Manages plugin content streaming with look-ahead buffering. Maintains a queue
|
||||
of plugin content that's ready to be rendered, prefetching 1-2 plugins ahead
|
||||
of the current scroll position.
|
||||
|
||||
Supports three display modes:
|
||||
- SCROLL: Continuous scrolling content
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by
|
||||
- STATIC: Pause scroll to display (marked for coordinator handling)
|
||||
Each plugin takes part in one of three ways (its Vegas participation, see
|
||||
BasePlugin.get_vegas_participation):
|
||||
- 'scroll': its content joins the strip
|
||||
- 'pause': the scroll pauses for its turn (a STATIC segment, marked for the
|
||||
coordinator)
|
||||
- 'exclude': left out of the rotation
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING, cast
|
||||
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from PIL import Image
|
||||
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode, resolve_vegas_participation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.plugin_system.plugin_manager import PluginManager
|
||||
@@ -34,11 +36,12 @@ class ContentSegment:
|
||||
"""One plugin's content for a cycle.
|
||||
|
||||
A STATIC segment carries no images: it marks where the coordinator pauses
|
||||
the scroll to show the plugin full-screen.
|
||||
the scroll to show a plugin whose participation is ``'pause'``. Every
|
||||
other segment is SCROLL.
|
||||
"""
|
||||
plugin_id: str
|
||||
images: List[Image.Image]
|
||||
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.FIXED_SEGMENT)
|
||||
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.SCROLL)
|
||||
|
||||
|
||||
class StreamManager:
|
||||
@@ -335,25 +338,14 @@ class StreamManager:
|
||||
logger.debug("[%s] Vegas: skipped (not enabled)", plugin_id)
|
||||
continue
|
||||
|
||||
# Content type 'none' is left out, except for STATIC plugins,
|
||||
# which pause the scroll rather than contributing to it.
|
||||
content_type = self.plugin_adapter.get_content_type(plugin, plugin_id)
|
||||
display_mode = VegasDisplayMode.FIXED_SEGMENT
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
except Exception:
|
||||
# Plugin error should not abort refresh; use default mode
|
||||
logger.exception(
|
||||
"[%s] (%s) get_vegas_display_mode() failed, using default",
|
||||
plugin_id, plugin.__class__.__name__
|
||||
)
|
||||
|
||||
included = (content_type != 'none'
|
||||
or display_mode == VegasDisplayMode.STATIC)
|
||||
# 'pause' plugins stay in the rotation: they pause the scroll
|
||||
# for their turn rather than contributing to it.
|
||||
participation = resolve_vegas_participation(plugin, plugin_id)
|
||||
included = participation != 'exclude'
|
||||
logger.debug(
|
||||
"[%s] Vegas: %s (content_type=%s, display_mode=%s)",
|
||||
"[%s] Vegas: %s (participation=%s)",
|
||||
plugin_id, "included" if included else "excluded",
|
||||
content_type, display_mode.value
|
||||
participation
|
||||
)
|
||||
if included:
|
||||
available_plugins.append(plugin_id)
|
||||
@@ -580,23 +572,13 @@ class StreamManager:
|
||||
logger.warning("[%s] Plugin not found in plugin_manager.plugins", plugin_id)
|
||||
return None
|
||||
|
||||
display_mode = VegasDisplayMode.FIXED_SEGMENT
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.debug(
|
||||
"[%s] get_vegas_display_mode() not available: %s (using FIXED_SEGMENT)",
|
||||
plugin_id, e
|
||||
)
|
||||
|
||||
# For STATIC mode, we create a placeholder segment
|
||||
# The actual content will be displayed by coordinator during pause
|
||||
if display_mode == VegasDisplayMode.STATIC:
|
||||
# Create minimal placeholder - coordinator handles actual display
|
||||
# A 'pause' plugin gets a placeholder segment; the coordinator
|
||||
# draws it with display() when the scroll reaches its turn.
|
||||
if resolve_vegas_participation(plugin, plugin_id) == 'pause':
|
||||
segment = ContentSegment(
|
||||
plugin_id=plugin_id,
|
||||
images=[], # No images needed for static pause
|
||||
display_mode=display_mode
|
||||
display_mode=VegasDisplayMode.STATIC
|
||||
)
|
||||
self.stats['segments_fetched'] += 1
|
||||
logger.debug(
|
||||
@@ -605,7 +587,6 @@ class StreamManager:
|
||||
)
|
||||
return segment
|
||||
|
||||
# Get content via adapter for SCROLL/FIXED_SEGMENT modes
|
||||
images = self.plugin_adapter.get_content(plugin, plugin_id)
|
||||
if not images:
|
||||
# The adapter already warns when every content path failed;
|
||||
@@ -619,13 +600,13 @@ class StreamManager:
|
||||
segment = ContentSegment(
|
||||
plugin_id=plugin_id,
|
||||
images=images,
|
||||
display_mode=display_mode
|
||||
display_mode=VegasDisplayMode.SCROLL
|
||||
)
|
||||
|
||||
self.stats['segments_fetched'] += 1
|
||||
logger.debug(
|
||||
"[%s] Segment: %d image(s), %dpx, mode=%s",
|
||||
plugin_id, len(images), total_width, display_mode.value
|
||||
"[%s] Segment: %d image(s), %dpx",
|
||||
plugin_id, len(images), total_width
|
||||
)
|
||||
return segment
|
||||
|
||||
@@ -693,16 +674,16 @@ class StreamManager:
|
||||
return layout
|
||||
|
||||
def is_static_plugin(self, plugin_id: str) -> bool:
|
||||
"""Whether a loaded plugin asks Vegas to pause for it (STATIC mode)."""
|
||||
"""Whether a loaded plugin asks Vegas to pause for it (participation 'pause').
|
||||
|
||||
Only 'pause' is acted on here. A plugin whose participation has turned
|
||||
to 'exclude' since the rotation was built is still fetched this cycle,
|
||||
as it always was; the next refresh drops it.
|
||||
"""
|
||||
plugin = getattr(self.plugin_manager, 'plugins', {}).get(plugin_id)
|
||||
if plugin is None:
|
||||
return False
|
||||
try:
|
||||
return cast(bool, plugin.get_vegas_display_mode() == VegasDisplayMode.STATIC)
|
||||
except Exception:
|
||||
logger.debug("[%s] get_vegas_display_mode() failed; treating as not STATIC",
|
||||
plugin_id, exc_info=True)
|
||||
return False
|
||||
return resolve_vegas_participation(plugin, plugin_id) == 'pause'
|
||||
|
||||
def take_next_group(
|
||||
self, count: Optional[int] = None, offscreen_only: bool = False
|
||||
|
||||
@@ -45,6 +45,19 @@ DEPRECATED = {
|
||||
"src.plugin_system.plugin_manager.PluginManager": ["get_enabled_plugins"],
|
||||
}
|
||||
|
||||
#: Deprecated with Vegas participation, for removal in 3.9.0: core never read
|
||||
#: them (src.plugin_system.base_plugin.VEGAS_LEGACY_REMOVAL).
|
||||
DEPRECATED_3_9 = {
|
||||
"src.plugin_system.base_plugin.BasePlugin": [
|
||||
"get_supported_vegas_modes", "get_vegas_segment_width",
|
||||
],
|
||||
}
|
||||
|
||||
#: Every pinned marker: (class path, method) -> the release that removes it.
|
||||
PINNED = {(path, name): removal
|
||||
for removal, table in (("3.8.0", DEPRECATED), ("3.9.0", DEPRECATED_3_9))
|
||||
for path, names in table.items() for name in names}
|
||||
|
||||
|
||||
def _cls(path):
|
||||
import importlib
|
||||
@@ -52,14 +65,14 @@ def _cls(path):
|
||||
return getattr(importlib.import_module(module), name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", sorted(DEPRECATED))
|
||||
@pytest.mark.parametrize("path", sorted({path for path, _ in PINNED}))
|
||||
def test_exactly_these_methods_are_deprecated(path):
|
||||
cls = _cls(path)
|
||||
marked = sorted(name for name, value in vars(cls).items()
|
||||
if hasattr(value, "__deprecated__"))
|
||||
assert marked == sorted(DEPRECATED[path])
|
||||
assert marked == sorted(name for owner, name in PINNED if owner == path)
|
||||
for name in marked:
|
||||
assert "3.8.0" in getattr(cls, name).__deprecated__
|
||||
assert f"LEDMatrix {PINNED[(path, name)]}" in getattr(cls, name).__deprecated__
|
||||
|
||||
|
||||
def _markers():
|
||||
@@ -82,7 +95,7 @@ def _markers():
|
||||
|
||||
|
||||
def test_markers_are_found():
|
||||
assert len(_markers()) == sum(len(v) for v in DEPRECATED.values())
|
||||
assert len(_markers()) == len(PINNED)
|
||||
|
||||
|
||||
def test_no_marker_names_a_release_already_shipped():
|
||||
@@ -116,8 +129,7 @@ def usage_script():
|
||||
def test_usage_script_lists_exactly_the_pinned_markers(usage_script):
|
||||
found = {(f"{m.module}.{m.owner}", m.method, m.removal)
|
||||
for m in usage_script.find_markers(REPO)}
|
||||
assert found == {(path, name, "3.8.0")
|
||||
for path, names in DEPRECATED.items() for name in names}
|
||||
assert found == {(path, name, removal) for (path, name), removal in PINNED.items()}
|
||||
|
||||
|
||||
def test_usage_script_tells_uses_from_name_collisions(usage_script, tmp_path):
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
"""Vegas participation: one declared 'scroll' | 'pause' | 'exclude' per plugin.
|
||||
|
||||
Before it existed Vegas read two hooks and branched on only part of each:
|
||||
get_vegas_content_type() == 'none' excluded a plugin (unless it was STATIC),
|
||||
get_vegas_display_mode() == STATIC paused the scroll for it, and nothing
|
||||
distinguished SCROLL from FIXED_SEGMENT. These tests pin the derivation from
|
||||
those hooks to exactly what the old StreamManager did, so every existing
|
||||
plugin keeps its behaviour, and cover the new ways to declare it.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
os.environ.setdefault("EMULATOR", "true")
|
||||
|
||||
from src import deprecation
|
||||
from src.plugin_system import base_plugin
|
||||
from src.plugin_system.base_plugin import (
|
||||
VEGAS_LEGACY_REMOVAL, VEGAS_PARTICIPATION_VALUES, BasePlugin, VegasDisplayMode,
|
||||
legacy_vegas_participation, resolve_vegas_participation,
|
||||
)
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
from src.vegas_mode.stream_manager import StreamManager
|
||||
from test._api_v3_test_helpers import ( # noqa: F401 - fixtures
|
||||
api_v3_client, api_v3_module,
|
||||
)
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
|
||||
class _Raises:
|
||||
"""A hook that raises ``exc`` when called."""
|
||||
|
||||
def __init__(self, exc):
|
||||
self.exc = exc
|
||||
|
||||
|
||||
def _duck(display_mode=_MISSING, content_type=_MISSING, **attrs):
|
||||
"""A plugin that is not a BasePlugin: only the hooks it is given."""
|
||||
ns = SimpleNamespace(enabled=True, **attrs)
|
||||
for name, value in (('get_vegas_display_mode', display_mode),
|
||||
('get_vegas_content_type', content_type)):
|
||||
if value is _MISSING:
|
||||
continue
|
||||
if isinstance(value, _Raises):
|
||||
def hook(exc=value.exc):
|
||||
raise exc
|
||||
else:
|
||||
def hook(value=value):
|
||||
return value
|
||||
setattr(ns, name, hook)
|
||||
return ns
|
||||
|
||||
|
||||
class _Plugin(BasePlugin):
|
||||
"""A real BasePlugin; class attributes override the legacy hooks."""
|
||||
|
||||
content_type = None
|
||||
display_mode = None
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def display(self, force_clear=False):
|
||||
pass
|
||||
|
||||
def get_vegas_content_type(self):
|
||||
if self.content_type is None:
|
||||
return super().get_vegas_content_type()
|
||||
return self.content_type
|
||||
|
||||
def get_vegas_display_mode(self):
|
||||
if self.display_mode is None:
|
||||
return super().get_vegas_display_mode()
|
||||
return self.display_mode
|
||||
|
||||
|
||||
def _plugin(config=None, manifest=None, cls=_Plugin, plugin_id='demo', **overrides):
|
||||
manifests = {plugin_id: manifest} if manifest is not None else {}
|
||||
plugin_manager = SimpleNamespace(plugin_manifests=manifests, plugins={})
|
||||
if overrides:
|
||||
cls = type('Custom', (cls,), overrides)
|
||||
return cls(plugin_id, dict(config or {}), MagicMock(), MagicMock(), plugin_manager)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _quiet_once_sets(monkeypatch):
|
||||
"""Each test sees first-time warnings afresh."""
|
||||
monkeypatch.setattr(deprecation, '_warned', set())
|
||||
monkeypatch.setattr(base_plugin, '_vegas_warned', set())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# What the StreamManager on origin/main (6047eb5e) decided, copied verbatim in
|
||||
# substance, so the derivation is checked against the old code rather than
|
||||
# against a restatement of the new one.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _old_included(plugin):
|
||||
content_type = 'static' # PluginAdapter.get_content_type
|
||||
if hasattr(plugin, 'get_vegas_content_type'):
|
||||
try:
|
||||
content_type = plugin.get_vegas_content_type()
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
pass
|
||||
display_mode = VegasDisplayMode.FIXED_SEGMENT # _refresh_plugin_list
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
except Exception:
|
||||
pass
|
||||
return content_type != 'none' or display_mode == VegasDisplayMode.STATIC
|
||||
|
||||
|
||||
def _old_paused(plugin): # is_static_plugin
|
||||
try:
|
||||
return plugin.get_vegas_display_mode() == VegasDisplayMode.STATIC
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
DISPLAY_MODES = [
|
||||
_MISSING, VegasDisplayMode.SCROLL, VegasDisplayMode.FIXED_SEGMENT,
|
||||
VegasDisplayMode.STATIC, None, 'static', 'scroll', _Raises(AttributeError('x')),
|
||||
]
|
||||
CONTENT_TYPES = [
|
||||
_MISSING, 'multi', 'static', 'single', 'none', None, _Raises(ValueError('x')),
|
||||
]
|
||||
|
||||
|
||||
def _label(value):
|
||||
if value is _MISSING:
|
||||
return 'missing'
|
||||
if isinstance(value, _Raises):
|
||||
return f'raises-{type(value.exc).__name__}'
|
||||
return repr(value)
|
||||
|
||||
|
||||
class TestLegacyDerivation:
|
||||
@pytest.mark.parametrize('display_mode', DISPLAY_MODES, ids=_label)
|
||||
@pytest.mark.parametrize('content_type', CONTENT_TYPES, ids=_label)
|
||||
def test_matches_what_the_old_stream_manager_did(self, display_mode, content_type):
|
||||
plugin = _duck(display_mode, content_type)
|
||||
participation = legacy_vegas_participation(plugin)
|
||||
assert (participation != 'exclude') == _old_included(plugin)
|
||||
assert (participation == 'pause') == _old_paused(plugin)
|
||||
|
||||
@pytest.mark.parametrize('display_mode, content_type, expected', [
|
||||
(VegasDisplayMode.STATIC, 'multi', 'pause'),
|
||||
(VegasDisplayMode.STATIC, 'none', 'pause'), # STATIC beats 'none'
|
||||
(VegasDisplayMode.SCROLL, 'none', 'exclude'),
|
||||
(VegasDisplayMode.FIXED_SEGMENT, 'none', 'exclude'),
|
||||
(VegasDisplayMode.SCROLL, 'multi', 'scroll'),
|
||||
(VegasDisplayMode.FIXED_SEGMENT, 'static', 'scroll'), # never told apart
|
||||
(VegasDisplayMode.FIXED_SEGMENT, 'single', 'scroll'),
|
||||
('static', 'multi', 'scroll'), # only the enum member ever paused
|
||||
])
|
||||
def test_the_table(self, display_mode, content_type, expected):
|
||||
assert legacy_vegas_participation(_duck(display_mode, content_type)) == expected
|
||||
|
||||
@pytest.mark.parametrize('vegas_mode, content_type, expected', [
|
||||
(None, None, 'scroll'), # BasePlugin defaults: 'static' / FIXED
|
||||
(None, 'multi', 'scroll'),
|
||||
(None, 'none', 'exclude'),
|
||||
('scroll', None, 'scroll'),
|
||||
('fixed', 'multi', 'scroll'),
|
||||
('static', None, 'pause'),
|
||||
('static', 'none', 'pause'),
|
||||
('bogus', 'none', 'exclude'), # an invalid vegas_mode is ignored
|
||||
])
|
||||
def test_base_plugin_defaults(self, vegas_mode, content_type, expected):
|
||||
config = {} if vegas_mode is None else {'vegas_mode': vegas_mode}
|
||||
plugin = _plugin(config, content_type=content_type)
|
||||
assert plugin.get_vegas_participation() == expected
|
||||
assert resolve_vegas_participation(plugin) == expected
|
||||
|
||||
def test_a_magicmock_plugin_scrolls(self):
|
||||
# Test doubles all over the suite are MagicMocks; they scrolled before.
|
||||
assert resolve_vegas_participation(MagicMock(), 'mock') == 'scroll'
|
||||
|
||||
|
||||
class TestDeclaredParticipation:
|
||||
@pytest.mark.parametrize('value', VEGAS_PARTICIPATION_VALUES)
|
||||
def test_user_config_wins_over_manifest_and_hooks(self, value):
|
||||
plugin = _plugin({'vegas_participation': value, 'vegas_mode': 'static'},
|
||||
manifest={'vegas_participation': 'exclude' if value != 'exclude'
|
||||
else 'scroll'},
|
||||
content_type='none')
|
||||
assert plugin.get_vegas_participation() == value
|
||||
assert resolve_vegas_participation(plugin) == value
|
||||
|
||||
@pytest.mark.parametrize('value', VEGAS_PARTICIPATION_VALUES)
|
||||
def test_manifest_wins_over_hooks(self, value):
|
||||
hooks_say = 'pause' if value != 'pause' else 'scroll'
|
||||
plugin = _plugin({'vegas_mode': 'static'} if hooks_say == 'pause' else {},
|
||||
manifest={'vegas_participation': value})
|
||||
assert legacy_vegas_participation(plugin) == hooks_say
|
||||
assert plugin.get_vegas_participation() == value
|
||||
|
||||
def test_case_and_whitespace_are_ignored(self):
|
||||
assert _plugin({'vegas_participation': ' Pause '}).get_vegas_participation() == 'pause'
|
||||
|
||||
@pytest.mark.parametrize('unset', [None, '', ' '])
|
||||
def test_empty_config_value_is_no_setting(self, unset):
|
||||
plugin = _plugin({'vegas_participation': unset, 'vegas_mode': 'static'})
|
||||
assert plugin.get_vegas_participation() == 'pause'
|
||||
|
||||
def test_invalid_config_value_is_ignored_and_logged_once(self, caplog):
|
||||
plugin = _plugin({'vegas_participation': 'fixed', 'vegas_mode': 'static'})
|
||||
with caplog.at_level(logging.WARNING):
|
||||
for _ in range(3):
|
||||
assert resolve_vegas_participation(plugin) == 'pause'
|
||||
warned = [r for r in caplog.records if 'Invalid vegas_participation' in r.getMessage()]
|
||||
assert len(warned) == 1
|
||||
|
||||
def test_invalid_manifest_value_is_ignored(self, caplog):
|
||||
plugin = _plugin({}, manifest={'vegas_participation': 'sometimes'}, content_type='none')
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert plugin.get_vegas_participation() == 'exclude'
|
||||
assert plugin.get_vegas_participation() == 'exclude'
|
||||
assert sum('manifest vegas_participation' in r.getMessage()
|
||||
for r in caplog.records) == 1
|
||||
|
||||
def test_a_plugin_manager_without_manifests_is_fine(self):
|
||||
plugin = _Plugin('demo', {}, MagicMock(), MagicMock(), None)
|
||||
assert plugin.get_vegas_participation() == 'scroll'
|
||||
|
||||
def test_an_override_is_used(self):
|
||||
plugin = _plugin(get_vegas_participation=lambda self: 'pause')
|
||||
assert resolve_vegas_participation(plugin) == 'pause'
|
||||
|
||||
def test_the_user_setting_beats_an_override(self):
|
||||
plugin = _plugin({'vegas_participation': 'exclude'},
|
||||
get_vegas_participation=lambda self: 'pause')
|
||||
assert resolve_vegas_participation(plugin) == 'exclude'
|
||||
|
||||
def test_an_override_returning_junk_falls_back_to_the_hooks(self, caplog):
|
||||
plugin = _plugin({'vegas_mode': 'static'},
|
||||
get_vegas_participation=lambda self: 'fixed')
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert resolve_vegas_participation(plugin) == 'pause'
|
||||
|
||||
def test_an_override_that_raises_falls_back_to_the_hooks(self):
|
||||
def boom(self):
|
||||
raise RuntimeError('broken')
|
||||
plugin = _plugin(content_type='none', get_vegas_participation=boom)
|
||||
assert resolve_vegas_participation(plugin) == 'exclude'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# StreamManager decides through participation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _stream(plugins):
|
||||
adapter = MagicMock()
|
||||
adapter.get_content.return_value = [Image.new('RGB', (20, 8))]
|
||||
return StreamManager(VegasModeConfig(), SimpleNamespace(plugins=plugins), adapter), adapter
|
||||
|
||||
|
||||
class TestStreamManager:
|
||||
def _plugins(self):
|
||||
return {
|
||||
'scroller': _plugin(plugin_id='scroller', content_type='multi'),
|
||||
'pauser': _plugin({'vegas_mode': 'static'}, plugin_id='pauser'),
|
||||
'hidden': _plugin(plugin_id='hidden', content_type='none'),
|
||||
'declared': _plugin({'vegas_participation': 'pause'}, plugin_id='declared'),
|
||||
'opted_out': _plugin({'vegas_participation': 'exclude'}, plugin_id='opted_out',
|
||||
content_type='multi'),
|
||||
}
|
||||
|
||||
def test_refresh_keeps_scroll_and_pause_and_drops_exclude(self):
|
||||
stream, _ = _stream(self._plugins())
|
||||
stream._refresh_plugin_list()
|
||||
assert sorted(stream._ordered_plugins) == ['declared', 'pauser', 'scroller']
|
||||
|
||||
def test_only_pause_plugins_are_static(self):
|
||||
stream, _ = _stream(self._plugins())
|
||||
assert {pid: stream.is_static_plugin(pid) for pid in self._plugins()} == {
|
||||
'scroller': False, 'pauser': True, 'hidden': False,
|
||||
'declared': True, 'opted_out': False}
|
||||
assert stream.is_static_plugin('not-loaded') is False
|
||||
|
||||
def test_swap_mode_fetch_makes_a_placeholder_for_pause(self):
|
||||
stream, adapter = _stream(self._plugins())
|
||||
pause = stream._fetch_plugin_content('declared')
|
||||
assert pause.display_mode == VegasDisplayMode.STATIC and pause.images == []
|
||||
adapter.get_content.assert_not_called()
|
||||
scroll = stream._fetch_plugin_content('scroller')
|
||||
assert scroll.display_mode == VegasDisplayMode.SCROLL and len(scroll.images) == 1
|
||||
|
||||
def test_continuous_mode_group_leaves_pause_plugins_unfetched(self):
|
||||
plugins = self._plugins()
|
||||
stream, adapter = _stream(plugins)
|
||||
group = dict(stream.take_next_group(count=10))
|
||||
assert group['declared'] == [] and group['pauser'] == []
|
||||
assert len(group['scroller']) == 1
|
||||
assert 'hidden' not in group and 'opted_out' not in group
|
||||
fetched = {call.args[1] for call in adapter.get_content.call_args_list}
|
||||
assert fetched == {'scroller'}
|
||||
|
||||
def test_a_user_can_make_a_static_plugin_scroll(self):
|
||||
plugin = _plugin({'vegas_mode': 'static', 'vegas_participation': 'scroll'},
|
||||
plugin_id='p')
|
||||
stream, _ = _stream({'p': plugin})
|
||||
assert stream.is_static_plugin('p') is False
|
||||
assert stream._fetch_plugin_content('p').display_mode == VegasDisplayMode.SCROLL
|
||||
|
||||
@pytest.mark.parametrize('display_mode', DISPLAY_MODES, ids=_label)
|
||||
@pytest.mark.parametrize('content_type', CONTENT_TYPES, ids=_label)
|
||||
def test_decisions_match_the_old_stream_manager(self, display_mode, content_type):
|
||||
plugin = _duck(display_mode, content_type)
|
||||
stream, _ = _stream({'p': plugin})
|
||||
stream._refresh_plugin_list()
|
||||
assert ('p' in stream._ordered_plugins) == _old_included(plugin)
|
||||
assert stream.is_static_plugin('p') == _old_paused(plugin)
|
||||
segment = stream._fetch_plugin_content('p')
|
||||
assert (segment.display_mode == VegasDisplayMode.STATIC) == _old_paused(plugin)
|
||||
|
||||
def test_a_display_mode_hook_that_raises_no_longer_drops_the_swap_segment(self):
|
||||
# The one intended difference: swap mode's fetch let anything but
|
||||
# AttributeError/TypeError from get_vegas_display_mode() abort the
|
||||
# fetch, while every other decision point treated it as "not STATIC".
|
||||
# All three now agree: the plugin scrolls.
|
||||
stream, _ = _stream({'p': _duck(_Raises(RuntimeError('x')), 'multi')})
|
||||
assert stream._fetch_plugin_content('p').display_mode == VegasDisplayMode.SCROLL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deprecations (removal in 3.9.0)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDeprecations:
|
||||
@pytest.mark.parametrize('name', ['get_supported_vegas_modes', 'get_vegas_segment_width'])
|
||||
def test_marked_for_3_9_0(self, name):
|
||||
marker = getattr(getattr(BasePlugin, name), '__deprecated__', '')
|
||||
assert 'LEDMatrix 3.9.0' in marker
|
||||
# The marker spells the version out; it must match the constant.
|
||||
assert f'LEDMatrix {VEGAS_LEGACY_REMOVAL}' in marker
|
||||
|
||||
def test_the_kept_hooks_are_not_deprecated(self):
|
||||
for name in ('get_vegas_content', 'get_vegas_render_width',
|
||||
'get_vegas_priority_weight', 'get_vegas_participation',
|
||||
'get_vegas_content_type', 'get_vegas_display_mode'):
|
||||
assert not hasattr(getattr(BasePlugin, name), '__deprecated__'), name
|
||||
|
||||
def test_calling_them_still_works_and_warns_once(self, caplog):
|
||||
plugin = _plugin({'vegas_panel_count': 2}, content_type='multi')
|
||||
with warnings.catch_warnings(record=True) as caught, \
|
||||
caplog.at_level(logging.WARNING):
|
||||
warnings.simplefilter('always')
|
||||
assert plugin.get_supported_vegas_modes() == [
|
||||
VegasDisplayMode.SCROLL, VegasDisplayMode.FIXED_SEGMENT]
|
||||
assert plugin.get_supported_vegas_modes()
|
||||
assert plugin.get_vegas_segment_width() == 2
|
||||
messages = [str(w.message) for w in caught]
|
||||
assert len(messages) == 2
|
||||
assert all('3.9.0' in m for m in messages)
|
||||
|
||||
def test_vegas_panel_count_warns_once_per_plugin(self, caplog):
|
||||
first = _plugin({'vegas_panel_count': 2}, plugin_id='a')
|
||||
second = _plugin({'vegas_panel_count': 3}, plugin_id='b')
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter('always')
|
||||
for _ in range(3):
|
||||
resolve_vegas_participation(first, 'a')
|
||||
resolve_vegas_participation(second, 'b')
|
||||
messages = [str(w.message) for w in caught]
|
||||
assert len(messages) == 2
|
||||
assert "plugin 'a'" in messages[0] and "plugin 'b'" in messages[1]
|
||||
assert all('removed in LEDMatrix 3.9.0' in m for m in messages)
|
||||
|
||||
def test_no_warning_without_the_key(self):
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter('always')
|
||||
resolve_vegas_participation(_plugin(), 'demo')
|
||||
assert caught == []
|
||||
|
||||
def test_warn_deprecated_is_once_per_key(self, caplog):
|
||||
with warnings.catch_warnings(record=True) as caught, \
|
||||
caplog.at_level(logging.WARNING):
|
||||
warnings.simplefilter('always')
|
||||
assert deprecation.warn_deprecated('Thing', '9.9.9', 'use other') is True
|
||||
assert deprecation.warn_deprecated('Thing', '9.9.9') is False
|
||||
assert deprecation.warn_deprecated('Thing', '9.9.9', once_key='k2') is True
|
||||
assert [str(w.message) for w in caught] == [
|
||||
'Thing is deprecated and will be removed in LEDMatrix 9.9.9; use other',
|
||||
'Thing is deprecated and will be removed in LEDMatrix 9.9.9',
|
||||
]
|
||||
assert caught[0].category is DeprecationWarning
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config schema and web API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSchema:
|
||||
def test_core_property_is_an_enum_without_a_default(self):
|
||||
from src.plugin_system.schema_manager import (
|
||||
CORE_PLUGIN_PROPERTIES, CORE_VEGAS_TUNING_KEYS, plugin_config_defaults)
|
||||
prop = CORE_PLUGIN_PROPERTIES['vegas_participation']
|
||||
assert prop['enum'] == list(VEGAS_PARTICIPATION_VALUES)
|
||||
# A default would be written into every plugin's config and override
|
||||
# what the plugin declares.
|
||||
assert 'default' not in prop
|
||||
assert 'vegas_participation' in CORE_VEGAS_TUNING_KEYS
|
||||
defaults = plugin_config_defaults({'type': 'object', 'properties': {}})
|
||||
assert 'vegas_participation' not in defaults
|
||||
|
||||
def test_validation_accepts_the_values_and_rejects_others(self):
|
||||
from src.plugin_system.schema_manager import SchemaManager
|
||||
manager = SchemaManager(plugins_dir='.')
|
||||
schema = {'type': 'object', 'additionalProperties': False,
|
||||
'properties': {'city': {'type': 'string'}}}
|
||||
for value in VEGAS_PARTICIPATION_VALUES:
|
||||
ok, errors = manager.validate_config_against_schema(
|
||||
{'vegas_participation': value}, schema, 'demo')
|
||||
assert ok, errors
|
||||
ok, _ = manager.validate_config_against_schema(
|
||||
{'vegas_participation': 'fixed'}, schema, 'demo')
|
||||
assert not ok
|
||||
|
||||
def test_manifest_schema_declares_it(self):
|
||||
import json
|
||||
from pathlib import Path
|
||||
schema = json.loads((Path(__file__).resolve().parent.parent / 'schema'
|
||||
/ 'manifest_schema.json').read_text(encoding='utf-8'))
|
||||
assert schema['properties']['vegas_participation']['enum'] == list(
|
||||
VEGAS_PARTICIPATION_VALUES)
|
||||
|
||||
|
||||
class TestInstalledPluginsApi:
|
||||
@pytest.fixture
|
||||
def installed(self, api_v3_module, api_v3_client, tmp_path):
|
||||
def _get(instance, config):
|
||||
api = api_v3_module.api_v3
|
||||
info = {'id': 'demo', 'name': 'Demo', 'version': '1.0.0', 'loaded': True}
|
||||
api.plugin_manager.plugins_dir = str(tmp_path)
|
||||
api.plugin_manager.get_all_plugin_info = MagicMock(return_value=[info])
|
||||
api.plugin_manager.get_plugin = MagicMock(return_value=instance)
|
||||
api.plugin_store_manager.get_registry_info = MagicMock(return_value=None)
|
||||
api.config_manager.load_config = MagicMock(return_value={'demo': config})
|
||||
response = api_v3_client.get('/api/v3/plugins/installed')
|
||||
assert response.status_code == 200
|
||||
return [p for p in response.get_json()['data']['plugins']
|
||||
if p['id'] == 'demo'][0]
|
||||
return _get
|
||||
|
||||
def test_a_loaded_plugin_reports_its_participation(self, installed):
|
||||
plugin = _plugin({'vegas_mode': 'static'})
|
||||
assert installed(plugin, plugin.config)['vegas_participation'] == 'pause'
|
||||
|
||||
def test_an_unloaded_plugin_reports_only_the_user_setting(self, installed):
|
||||
assert installed(None, {'vegas_participation': 'exclude'})[
|
||||
'vegas_participation'] == 'exclude'
|
||||
assert installed(None, {})['vegas_participation'] is None
|
||||
|
||||
@@ -31,7 +31,6 @@ class _Plugin:
|
||||
|
||||
def _stream(plugins):
|
||||
adapter = MagicMock()
|
||||
adapter.get_content_type.return_value = 'multi'
|
||||
adapter.get_content.return_value = [Image.new('RGB', (20, 8))]
|
||||
return StreamManager(VegasModeConfig(), SimpleNamespace(plugins=plugins), adapter)
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ from web_interface.blueprints.api_v3 import (
|
||||
jsonify, logger, os, request, subprocess, success_response,
|
||||
)
|
||||
from src.common.path_safety import safe_path_component
|
||||
from src.plugin_system.base_plugin import (
|
||||
configured_vegas_participation, resolve_vegas_participation,
|
||||
)
|
||||
import web_interface.blueprints.api_v3 as _pkg
|
||||
# Read through the module rather than bound by value: tests patch these
|
||||
# as module attributes, and a value binding would not see the patch.
|
||||
@@ -129,6 +132,14 @@ def get_installed_plugins():
|
||||
if 'vegas_mode' in plugin_config:
|
||||
vegas_mode = plugin_config['vegas_mode']
|
||||
|
||||
# What Vegas actually does with the plugin: 'scroll', 'pause' or
|
||||
# 'exclude'. The same resolution the ticker uses; without a loaded
|
||||
# instance only the user's own setting is known.
|
||||
if plugin_instance is not None:
|
||||
vegas_participation = resolve_vegas_participation(plugin_instance, plugin_id)
|
||||
else:
|
||||
vegas_participation = configured_vegas_participation(plugin_id, plugin_config)
|
||||
|
||||
return {
|
||||
'id': plugin_id,
|
||||
'name': plugin_info.get('name', plugin_id),
|
||||
@@ -154,6 +165,7 @@ def get_installed_plugins():
|
||||
'web_ui_actions': plugin_info.get('web_ui_actions', []),
|
||||
'vegas_mode': vegas_mode,
|
||||
'vegas_content_type': vegas_content_type,
|
||||
'vegas_participation': vegas_participation,
|
||||
}
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* orderInputId: 'vegas_plugin_order_value', // hidden input, JSON array of ids
|
||||
* excludedInputId: 'vegas_excluded_plugins_value', // optional: adds an
|
||||
* // include-checkbox per row; unchecked ids collect here (JSON array)
|
||||
* showVegasModeBadge: true // optional: Scroll/Fixed/Static badge
|
||||
* showVegasModeBadge: true // optional: Scroll/Pause/Excluded badge
|
||||
* });
|
||||
*
|
||||
* The container re-renders from /api/v3/plugins/installed each init; the
|
||||
@@ -21,10 +21,14 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Keyed by Vegas participation ('scroll' | 'pause' | 'exclude'); 'fixed'
|
||||
// and 'static' are the legacy vegas_mode values, for an older API.
|
||||
const MODE_LABELS = new Map([
|
||||
['scroll', { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' }],
|
||||
['fixed', { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' }],
|
||||
['static', { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }]
|
||||
['scroll', { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' }],
|
||||
['pause', { label: 'Pause', icon: 'fa-pause', color: 'text-orange-600' }],
|
||||
['exclude', { label: 'Excluded', icon: 'fa-ban', color: 'text-gray-500' }],
|
||||
['fixed', { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' }],
|
||||
['static', { label: 'Pause', icon: 'fa-pause', color: 'text-orange-600' }]
|
||||
]);
|
||||
|
||||
function init(options) {
|
||||
@@ -165,11 +169,11 @@
|
||||
}
|
||||
|
||||
if (options.showVegasModeBadge) {
|
||||
const vegasMode = plugin.vegas_mode || plugin.vegas_content_type || 'fixed';
|
||||
const modeInfo = MODE_LABELS.get(vegasMode) || MODE_LABELS.get('fixed');
|
||||
const vegasMode = plugin.vegas_participation || plugin.vegas_mode || 'scroll';
|
||||
const modeInfo = MODE_LABELS.get(vegasMode) || MODE_LABELS.get('scroll');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `text-xs ${modeInfo.color} ml-2`;
|
||||
badge.title = `Vegas display mode: ${modeInfo.label}`;
|
||||
badge.title = `Vegas participation: ${modeInfo.label}`;
|
||||
const badgeIcon = document.createElement('i');
|
||||
badgeIcon.className = `fas ${modeInfo.icon} mr-1`;
|
||||
badge.appendChild(badgeIcon);
|
||||
|
||||
Reference in New Issue
Block a user