From b1b08a17a9435e733001027a6a6bb3f7a449e136 Mon Sep 17 00:00:00 2001 From: Retr0 <202583450066@nuist.edu.cn> Date: Sat, 15 Aug 2026 12:40:50 +0800 Subject: [PATCH] plugin update --- CONTRIBUTING.md | 2 + js-plugin-api.md | 10 +++- plugins.json | 20 +++++++ plugins/.DS_Store | Bin 8196 -> 14340 bytes plugins/titan_danmaku_renderer/README.md | 20 ++++++- .../titan_danmaku_renderer.js | 49 +++++++++++++----- scripts/sync_plugins_index.py | 13 +++++ 7 files changed, 97 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce5cd75..7788b8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,6 +79,8 @@ const pluginManifest = { - 发布到市场的外部依赖应固定版本,并填写 64 位 SHA-256;不要引用会变化的分支或 `latest` 文件。 - `pluginDanmakuRenderers` 必须同时申请 `danmaku.renderer` 和 `script.external`。 - 渲染器当前只支持 `apiVersion: 1` 和 `platforms: ['android', 'ios']` 的子集;请勿提前声明 Windows。 +- 声明 `supportsRealtimeAdd: true` 的渲染器必须处理 `add` 消息,并立即显示 `message.item`,不能通过清空整表来实现。 +- 读取 `settings.value.rendererSettings` 时必须容忍对象或字段缺失,并忽略未知字段;渲染器专属设置不得改变其他渲染器的默认设置行为。 - 渲染器引用的每个依赖 ID 必须存在于 `pluginManifest.requires`。 - 外部代码的来源、许可和再分发条件由投稿者负责确认,并应在插件 README 中说明。 diff --git a/js-plugin-api.md b/js-plugin-api.md index 39f95e7..83657d1 100644 --- a/js-plugin-api.md +++ b/js-plugin-api.md @@ -206,6 +206,7 @@ const pluginDanmakuRenderers = [ description: '示例 DOM 弹幕引擎', apiVersion: 1, platforms: ['android', 'ios'], + supportsRealtimeAdd: true, requires: ['engine'], bootstrap: String.raw` const root = document.getElementById('nipa-danmaku-root'); @@ -217,6 +218,9 @@ const pluginDanmakuRenderers = [ case 'load': engine.load(message.items || []); break; + case 'add': + engine.addRealtime(message.item); + break; case 'settings': engine.setOptions(message.value || {}); break; @@ -241,6 +245,7 @@ const pluginDanmakuRenderers = [ - `description`:选填,渲染器说明。 - `apiVersion`:选填,默认 `1`;当前宿主只支持 `1`。 - `platforms`:必填,当前只接受 `android`、`ios`,至少填写一个。Windows 尚无 WebView 渲染宿主,声明 `windows` 也不会生效。 +- `supportsRealtimeAdd`:选填,默认 `false`。设为 `true` 后,本地发送成功导致的单条列表更新会改发 `add`,适配层必须立即渲染该条弹幕;未声明时仍通过整表 `load` 保持兼容。 - `requires`:选填,引用 `pluginManifest.requires` 的依赖 ID。省略时加载清单中的全部依赖;填写后仍按清单顺序加载选中的依赖。 - `bootstrap`:必填,外部脚本加载完成后执行的适配代码。必须安装 `window.NipaDanmakuRenderer.handle(message)`。 @@ -250,6 +255,7 @@ const pluginDanmakuRenderers = [ |---|---|---| | `initialize` | `apiVersion`、`pluginId`、`rendererId` | 初始化协议与身份信息 | | `load` | `version`、`items` | 弹幕列表变化时推送;`version` 是宿主列表版本 | +| `add` | `item` | 仅向声明 `supportsRealtimeAdd: true` 的渲染器发送本地新弹幕;不会紧接着发送同版本整表 `load` | | `settings` | `value` | 弹幕显示设置变化时推送 | | `clock` | `positionSeconds`、`durationSeconds`、`playing`、`playbackRate`、`seekRevision` | 播放时钟,正常播放时最多每 100 ms 推送一次 | | `dispose` | 无 | 释放引擎、DOM、Observer 和监听器 | @@ -260,7 +266,9 @@ const pluginDanmakuRenderers = [ - `color`(CSS `rgb(...)`)、`isMe`; - 可选的 `senderId`、`danmakuId`、`timestamp`、`source`、`fontSize`、`pool`、`weight`,以及数据源扩展字段。 -`settings.value` 当前包含 `visible`、`opacity`、`fontSize`、`fontFamily`、`displayArea`、`scrollDurationSeconds`、`stacking`、`merge`、`blockTop`、`blockBottom`、`blockScroll`、`blockWords`、`timeOffsetSeconds`。 +`settings.value` 当前包含 `visible`、`opacity`、`fontSize`、`fontFamily`、`displayArea`、`scrollDurationSeconds`、`stacking`、`merge`、`blockTop`、`blockBottom`、`blockScroll`、`blockWords`、`timeOffsetSeconds`。可选的 `rendererSettings` 是当前渲染器的专属配置对象;目前宿主仅在选择 Titan 时发送,其中包含 `opacity`、`fontSize`、`bold`、`fontBorder`、`fontFamily`、`speedPlus`、`density`、`duration`、`limit`、`preventShade`、`offsetTop`、`offsetBottom`、`maxLength`、`isRecyclingDom`、`isRecyclingModel`、`forbidShrinkState`。适配器应忽略不认识的字段,并为缺失字段保留自身默认值。Titan 的 `speedSync` 固定为 `true`,不作为可持久化设置发送。 + +Titan 专属设置在宿主中独立持久化,仅在 Titan 被选中时显示;其他渲染器继续使用原有通用设置入口和值域。 适配层需要自行把这些通用消息映射到第三方引擎。可通过以下通道向宿主写日志或报告运行错误: diff --git a/plugins.json b/plugins.json index 16b37c4..e3f2903 100644 --- a/plugins.json +++ b/plugins.json @@ -40,6 +40,26 @@ "author": "Heylyx841", "github": "", "file": "plugins/heylyx841.danmaku_limiter/heylyx841.danmaku_limiter.js" + }, + { + "id": "steam_status_sync", + "name": "Steam 状态同步", + "version": "2.0.0", + "minHostVersion": "1.11.3", + "description": "注意:使用前务必阅读文档!将当前观看的番剧信息同步到 Steam 状态。利用 NipaPlay 内置的远程控制 API 提供观看信息,配合 NipaPlay-Steam-Bridge 独立程序设置 Steam 状态。", + "author": "Retr0", + "github": "", + "file": "plugins/steam_status_sync/steam_status_sync.js" + }, + { + "id": "titan_danmaku_renderer", + "name": "JavaScript弹幕引擎", + "version": "1.0.5", + "minHostVersion": "1.11.4", + "description": "注意:仅支持 iOS、Android 端;使用实验性 JavaScript 弹幕引擎渲染 NipaPlay 弹幕", + "author": "Retr0", + "github": "", + "file": "plugins/titan_danmaku_renderer/titan_danmaku_renderer.js" } ] } diff --git a/plugins/.DS_Store b/plugins/.DS_Store index dcc9c78af969e3806c7e535e0b234e1cef8337af..b92896f6f8f606982f506f0619867f4e414ed790 100644 GIT binary patch literal 14340 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 delta 215 zcmZoEXmOBWU|?W$DortDU;r^WfEYvza8E20o2Vx_*}#NF63Ay@0b)j=I0Hi}LlHwf zLkdG8L*8aVsb8!c8&)!M0;NEJgBvI$&azpZ=Ls_-%VZt}J)WfU;)0}{{3M|4j>!dr zh1?R=)kX%EItoT+=93R ({ + function toTitanItem(item, index, realtime) { + const titanItem = { text: item.content || '', - stime: (+item.time || 0) * 1000, mode: typeToMode[item.type] || +item.originalType || 1, size: +item.fontSize || 25, color: colorToInt(item.color), - dmid: item.danmakuId || ('nipaplay-' + index), - })); + dmid: item.danmakuId || (realtime ? 'local-' + Date.now() : 'nipaplay-' + index), + }; + if (!realtime) titanItem.stime = (+item.time || 0) * 1000; + return titanItem; + } + function load(items) { + const titanItems = items.map((item, index) => toTitanItem(item, index, false)); rollLayer.textContent = ''; engine.clear(); engine.reset(); @@ -110,16 +116,30 @@ const pluginDanmakuRenderers = [ } engine.seek(clock.positionSeconds + clock.offsetSeconds); } + function addRealtime(item) { + engine.add(toTitanItem(item || {}, 0, true)); + } function applySettings(value) { + const rendererSettings = value.rendererSettings || {}; engine.setSetting('visible', value.visible !== false); - engine.setSetting('opacity', value.opacity == null ? 1 : value.opacity); + engine.setSetting('opacity', rendererSettings.opacity == null ? 0.85 : rendererSettings.opacity); engine.setSetting('area', Math.round((value.displayArea || 1) * 100)); - engine.setSetting( - 'fontFamily', - value.fontFamily - ? value.fontFamily + ', ' + simplifiedChineseFontFamily - : simplifiedChineseFontFamily, - ); + engine.setSetting('fontSize', rendererSettings.fontSize == null ? 1 : rendererSettings.fontSize); + engine.setSetting('bold', rendererSettings.bold !== false); + engine.setSetting('fontBorder', rendererSettings.fontBorder == null ? 0 : rendererSettings.fontBorder); + engine.setSetting('fontFamily', rendererSettings.fontFamily || defaultTitanFontFamily); + engine.setSetting('speedPlus', rendererSettings.speedPlus == null ? 1 : rendererSettings.speedPlus); + engine.setSetting('density', rendererSettings.density == null ? 1 : rendererSettings.density); + engine.setSetting('duration', rendererSettings.duration == null ? 4.5 : rendererSettings.duration); + engine.setSetting('limit', rendererSettings.limit == null ? 300 : rendererSettings.limit); + engine.setSetting('speedSync', true); + engine.setSetting('preventShade', rendererSettings.preventShade === true); + engine.setSetting('offsetTop', rendererSettings.offsetTop || 0); + engine.setSetting('offsetBottom', rendererSettings.offsetBottom || 0); + engine.setSetting('maxLength', rendererSettings.maxLength == null ? 50 : rendererSettings.maxLength); + engine.setSetting('isRecyclingDom', rendererSettings.isRecyclingDom !== false); + engine.setSetting('isRecyclingModel', rendererSettings.isRecyclingModel === true); + engine.setSetting('forbidShrinkState', rendererSettings.forbidShrinkState !== false); const nextOffset = +value.timeOffsetSeconds || 0; if (clock.offsetSeconds !== nextOffset) { clock.offsetSeconds = nextOffset; @@ -146,6 +166,7 @@ const pluginDanmakuRenderers = [ handle(message) { switch (message.type) { case 'load': load(message.items || []); break; + case 'add': addRealtime(message.item); break; case 'settings': applySettings(message.value || {}); break; case 'clock': syncClock(message); break; case 'dispose': diff --git a/scripts/sync_plugins_index.py b/scripts/sync_plugins_index.py index b6f4d45..6f3dfce 100644 --- a/scripts/sync_plugins_index.py +++ b/scripts/sync_plugins_index.py @@ -273,6 +273,16 @@ def _read_int_property(object_source, name, default=None): return int(match.group(0)) +def _read_bool_property(object_source, name, default=None): + start = _find_property(object_source, name) + if start is None: + return default + match = re.match(r"(?:true|false)\b", object_source[start:]) + if not match: + raise ValueError(f"'{name}' must be a boolean") + return match.group(0) == "true" + + def _read_string_array_property(object_source, name, default=None): start = _find_property(object_source, name) if start is None: @@ -332,6 +342,9 @@ def parse_danmaku_renderers(filepath): "platforms": _read_string_array_property( object_source, "platforms", [] ), + "supportsRealtimeAdd": _read_bool_property( + object_source, "supportsRealtimeAdd", False + ), "requires": _read_string_array_property( object_source, "requires", None ),