跳至主要内容

Contributing web UI with Plugins

插件可以在两个地方为 Owncast 添加自己的用户界面:管理员内的选项卡(针对主播的设置)和流下的操作按钮(针对观众的操作)。 这两者都在您的清单中声明,并由主机管理。 您提供内容,Owncast 将其放入正确的用户界面。

此页面上的清单声明是普通 JSON,无论您使用哪种语言编写都是一样的。 动态内容处理程序和运行时调用显示了两个 SDK 的内容。 有关安装和设置的信息,请参阅 JavaScriptPython

管理员页面

Owncat suggests只需要一个设置表单?

对于扁平的、类型化的设置(字符串、数字、开关),声明一个清单 config 块,让 Owncast 为您渲染表单。 请参阅 配置。 当您需要一个无法用自动表单表达的用户界面时,构建一个自定义的管理员页面。

插件可以注册出现在 Owncast 管理用户界面的页面下的 插件。 Declare them as an object keyed by plugin-relative path glob:

{
"permissions": ["http.serve"],
"admin": {
"pages": {
"/admin": { "title": "Settings", "icon": "gear" }
}
}
}

Each entry has:

Part备注
object keyRequired path glob under /plugins/\<your-slug>/. 示例:"/admin""/admin/*""/admin/api/*"
titleRequired tab label inside the plugin's admin view.
iconOptional short semantic name. 支持的图标:gearwrenchuseruserslockinfoappsdocsbell(别名如 settingsnotifications 也有效)。

The host derives the page path from the object key. Do not add a path member to the value. The host rejects arrays and page values containing the legacy path member.

它们的渲染方式

Owncast 的管理界面将每个声明的页面呈现为位于 /admin/plugins/configure?id=\<your-slug> 内的选项卡。 The tab body is an \<iframe> pointed at the path from the object key under /plugins/\<your-slug>/. 每个插件都可以获得一个可书签的 URL 和在管理导航中 插件 下的侧边栏条目。

插件的管理员页面呈现在管理员内部的选项卡,并且与其说明和权限选项卡并排。

主机会将基础样式表自动注入到管理员路径的 HTML 响应中,因此普通的 <input>\<button> 控件看起来对 Owncast 的管理员来说很原生,而无需您提供 CSS。 请参阅 样式插件用户界面,了解您免费获得的内容和可用的帮助类。 希望拥有自己样式的插件可以在其上叠加。

沙盒

页面在沙盒 \<iframe> 中运行。 您的脚本运行,表单提交,和同源 fetch 到您自己的 /plugins/\<your-slug>/ 端点正常工作。 页面还可以 打开弹出窗口触发文件下载(例如,您从脚本点击的 blob 或数据-URL \<a download>),并使用 confirm() / alert() / prompt() 对话框。 沙盒是您通常会注意到的唯一约束。 如果某个浏览器特性似乎被悄悄阻止,首先检查 iframe 沙盒。

认证保护

对清单声明的管理员路径的请求由主机进行认证保护。 未经身份验证的请求在您的插件代码运行之前会返回 401。 您不必检查这些路径上的请求认证。

匹配路径下的静态文件和动态端点都受到认证保护。 相同的保护适用于您的 public/admin/index.htmlPOST /admin/api/save-settings

当您同时拥有用户界面页面和 JSON API 时,请使用多个模糊匹配:

{
"admin": {
"pages": {
"/admin": { "title": "Settings" },
"/admin/*": { "title": "Settings" }
}
}
}

The admin UI deduplicates tabs by the resolved iframe URL, not by title. /admin and /admin/* both resolve to /admin/, so this pair produces one visible tab that gates the whole subtree. A pair like /admin and /admin/api/* resolves to two different URLs and produces two tabs. JSON object order is not significant. Owncast processes and displays pages in lexicographic path order.

作者流程

  1. 将管理员 HTML、CSS 和 JS 放在 public/admin/index.html(及其相关文件)中。
  2. 通过您在 /admin/api/... 的请求处理程序暴露管理员 API(请参阅 提供 HTTP)。
  3. Declare the relevant path keys in manifest.admin.pages.
  4. 在管理员用户界面中访问 /admin/plugins/configure?id=\<your-slug>。 Owncast 使用您现有的管理员登录来保护该页面。 不需要额外提示。

操作按钮

Owncast 在其观看者用户界面中显示一排操作按钮。 可点击的条目可以打开一个 URL(在模态或新选项卡中)或呈现原始 HTML。 插件可以贡献它们自己的。

一排插件贡献的操作按钮位于观众页面的流下,紧挨着内置的关注和通知按钮。

清单声明的按钮

{
"permissions": ["ui.modify", "http.serve"],
"actions": [
{
"title": "Chat Overlay",
"description": "Open the live chat overlay",
"url": "/",
"icon": "/star.png",
"color": "#3b82f6"
},
{
"title": "Issue tracker",
"url": "https://github.com/example/my-plugin/issues",
"openExternally": true
},
{
"title": "About this stream",
"html": "<p>Live every weekday at 8pm UTC.</p>"
}
]
}

当您的插件启用时,主机会将其操作条目与 Owncast 已经在流下显示的列表合并。 当禁用时,它们会消失。

字段参考

字段备注
title必填。 按钮标签。
url绝对 https://... URL 或路径。 与 html 互斥。
html在内联模态中呈现的原始 HTML。 与 url 互斥。
icon按钮上显示的可选图像 URL。 与 url 相同的路径规则。
color按钮背景的可选十六进制颜色。
description可选。 在基于 URL 的操作打开的模态中显示。
openExternally如果 true,则 URL 在新选项卡中打开,而不是在内联模态中打开。

路径规则

两个简单规则涵盖所有内容:

  • 相对路径自动前缀到您插件的命名空间。 "/" 变为 /plugins/my-plugin/"/star.png" 变为 /plugins/my-plugin/star.png。 这可以让您避免硬编码插件名称。 适用于 urlicon
  • 绝对 https://... URL 原封不动进行传递。 用于外部链接和 CDN 托管的图标。

主机强制执行:

  • 需要 ui.modify 权限。 具有 actions 的清单但没有 ui.modify 在加载时会被拒绝。
  • 每个条目必须恰好有一个 urlhtml
  • 解析到您命名空间的 URL 和图标需要 http.serve。 您是在提供它们。
  • 指向其他插件的命名空间的 URL 和图标会被拒绝。 捕获错误并防止一个插件宣传另一个插件的用户界面。

运行时新增

插件可以在运行时追加更多操作按钮,而无需重新加载,方法是调用 owncast.actions.add(...),并传递单个操作或它们的数组。 每个运行时条目经过与 manifest.actions 相同的验证,并保存在插件的配置中,因此添加内容可以在重新加载时保留。 owncast.actions.clear() 删除每个运行时添加。 声明的操作保持不变。

const { definePlugin, owncast } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onStreamStarted() {
owncast.actions.add({
title: 'Donate',
url: 'https://example.com/donate',
openExternally: true,
});
// or add several at once: owncast.actions.add([ { ... }, { ... } ])
},
});

一种常见模式是一个管理员页面,让主播添加自定义按钮(标签 + URL)叠加在插件的默认值之上。 SDK 中的 action-buttons 示例会提供这个功能的工作版本。

样式插件用户界面

Owncast 将基础样式表注入到每个在 iframe 中渲染的插件表面中:您的 管理员页面 和您的 观看者页面选项卡。 它是从 Owncast 自己的设计令牌构建的,因此普通的语义 HTML 在没有您自己 CSS 的情况下采用原生外观。

  • 标题、段落和链接会选择主题字体和颜色。
  • <input>\<textarea>\<select>\<button> 看起来和原生控件一样。 一个 \<button> 会获得主要样式。 添加 class="secondary" 用于轮廓变体。
  • \<table>\<fieldset>\<code> / \<pre> 具有合理的原生样式。

您的内容在页面上 平整呈现。 iframe 背景是透明的,因此主机的面板可以透过,与内置的关于和关注者选项卡呈现的方式相同。 您不会获得,并且不应该添加,遮挡的页面背景或将所有内容环绕的盒子。 这种平整的渲染使插件选项卡看起来像是 Owncast 的一部分,而不是嵌入的框架。

Owncat informs you它适用的地方

基础样式适用于 iframe 渲染的表面:管理员页面观看者页面选项卡。 您直接注入到观看者页面中的内容(extraPageContentscripts)会在真实的页面 DOM 中渲染,并继承 Owncast 的实际样式。

帮助类

为了超越普通元素的原生构建块,基础样式提供了一些可选择的类。 它们参考与 Owncast 其他部分相同的主题令牌,因此,当管理员自定义主题时,它们会自动重新样式。

它的作用
card一种原生卡片表面,外观与关注者和精选流卡片相同。 普通的 \<section> / \<article> 保持平整,因此当您希望获得盒装表面时,可以选择使用 class="card"
card interactiveinteractive 添加到可点击卡片以获得本地悬浮升起效果。
card-grid一种响应式网格,充填尽可能多的 ~260px 列,并在狭窄的框架上折叠为一列。 直接放入 card 子项。
tag匹配本地流卡片上的标签或徽章。
stack具有一致间隔的垂直 flex 列。
row具有一致间隔的水平 flex 行。
muted去重音文本,适用于标题和附加细节。
<div class="card-grid">
<article class="card interactive">
<h3>Album A</h3>
<p class="muted">Artist A</p>
<div class="row">
<span class="tag">jazz</span>
<span class="tag">2024</span>
</div>
</article>
<article class="card interactive">
<h3>Album B</h3>
<p class="muted">Artist B</p>
</article>
</div>

这里的所有内容都是可选的。 一个仅发送语义 HTML 的标签看起来就像本地应用。 在你想要卡片、网格或标签而不手动复制 Owncast 的值时,请使用帮助程序,并在你需要一些基本未覆盖的内容时在其上叠加你自己的 CSS(参见 观看器样式表)。

观看器样式表

插件可以通过捆绑 CSS 文件并将其列入 manifest.styles 来主题化查看页面。 主机将每个文件的内容内联到页面上的一个单一插件样式块中,因此插件在页面上扩展 CSS,而每个贡献不需要自己的 <link> 标签。

{
"permissions": ["ui.modify"],
"styles": ["theme.css", "overrides.css"]
}

仅需 ui.modify(插件在 Owncast 的界面内部绘制)。 http.serve isn't needed: the host reads each file from your plugin's assets/ directory and inlines the bytes into the page's plugin styles block on /api/config, not at a URL.

路径规则

  • "theme.css" 这样的裸路径自动前缀到你的插件命名空间。
  • "/theme.css" 的解析方式相同。
  • 完全合格的 /plugins/\<your-slug>/... 路径通过。
  • 其他插件命名空间中的路径会被拒绝。
  • http://https:// 的 URL 会被拒绝。 绑定外部资产,并从你的 CSS 中用 @font-faceurl(...) 进行引用,使得管理员在审查清单时看到每个将要加载的文件。
  • 每个条目必须以 .css 结尾。

贡献是如何呈现的

The host reads each file at request time and concatenates the bytes in front of an /* plugin: \<your-slug> ... */ comment, so devtools "view source" attributes a rule back to the plugin that shipped it. 禁用插件会在下次页面加载时丢弃其贡献。

CSS 体现在实时查看器 DOM 上运行,因此你的选择器会针对页面呈现的任何内容。 在单个根 ID 下作用域每条规则是一种值得保留的防御性习惯。 没有它,你的规则可能匹配主机页面渲染的元素,并产生意想不到的回归。

插件样式在层级中的位置

查看器页面的外观来自四个层级,按此顺序应用。 后面的层胜出。

  1. Owncast 的内置默认值。
  2. 插件样式:首先是你的 manifest.styles 文件,然后是你的 onPageStyles 输出。
  3. 管理员的外观变量,由 常规设置 → 外观 下的选择器设置的颜色。
  4. 管理员的自定义 CSS,那同一页面上的编辑器。

你的样式是第 2 层,因此管理员在第 3 和第 4 层的明确选择将覆盖你们在任何共同设置的属性上。 将主题视为基础,而不是最终字:

  • 你设置的一个令牌,管理员保留为默认值,显示你的值。
  • 你设置的一个令牌,管理员也设置,显示管理员的值。

部分和完整的主题都可以。 仅重新着色链接的插件会保持每种其他颜色不变。 设置整个调色板的插件仍会屈服于管理员选择的任何单个颜色。 管理员仍然控制自己的实例,外观页面告诉他们一个插件在参与:它显示了一个通告,命名你的插件,并标记你设置的每种颜色与 也由 \<plugin> 设置 的备注。 For that flagging to work, declare your colors as --theme-color-* custom properties in a :root { ... } block, the same form the admin's pickers write.

一个逃生阀打破了顺序:标记为 !important 的插件规则无论层次如何都会击败管理员的正常声明。 如果你希望管理员保留对其颜色的最终决定,请在主题 CSS 中避免使用它。

警告:CSS 中的相对 URL

插件 CSS 中的 url(...) 引用解析针对查看器页面,而不是针对插件的命名空间。 如果想引用打包的图像,请使用绝对路径 /plugins/\<your-slug>/logo.png,而不是 ./logo.png。 对于 @font-face 源也是一样。 插件的静态 URL 空间保持服务,因此直接引用有效,即使没有 <link> 指向该文件。

动态样式表:onPageStyles

当 CSS 依赖于插件状态、管理员挑选的主题或 KV 存储中的值时,从 onPageStyles 处理程序返回它,而不是(或与)静态文件一起返回。 没有用于此的清单字段。 The host calls the handler once per /api/config for any plugin that holds ui.modify and exports it, then appends what it returns to your plugin styles block after the static manifest.styles files. 在你的插件自己的样式中,后面的规则胜出,所以从 onPageStyles 仅返回活动覆盖就足够。 整个块仍然位于管理员的外观设置之下(参见 插件样式在层级中的位置)。

const ACCENTS = { ocean: '#2386e2', forest: '#42bea6' };

module.exports = definePlugin({
onPageStyles() {
const accent = ACCENTS[owncast.kv.get('theme')];
if (!accent) return;
return `:root { --theme-color-action: ${accent}; }`;
},
});

需要 ui.modify。 The examples above also read the KV store, which separately requires storage.kv. 在给定请求时,无需返回任何内容(简单的 return,与返回 "" 相同)。 调用不需要每个查看者参数,因此 /api/config 响应保持可缓存。 SDK 中的 theme-hub 示例使用它为整个查看器 UI 应用管理员选择的主题。

查看器脚本

插件可以通过捆绑 JavaScript 文件并将其列入 manifest.scripts 来扩展查看页面的运行时。 每个文件的内容被附加到 Owncast 已经为管理员的自定义 JS 提供的 /customjavascript 响应中,因此插件在页面行为上扩展,而每个贡献不需要自己的 \<script> 标签。

{
"permissions": ["ui.modify"],
"scripts": ["client.js"]
}

styles 相同的权限和路径规则,适用于单个 .js 条目(仅需要 ui.modify,主机从 assets/ 读取并内联到 /customjavascript)。 每个贡献都以 // plugin: \<your-slug> ... 注释作为前缀。

这些是 查看器页面 脚本,在浏览器中运行,始终为 JavaScript,无论你用哪个语言编写服务器端插件。

执行上下文

查看器页面加载 /customjavascript 作为一个单一的 \<script async> 标签。 每个插件的 JS 在与管理员的自定义 JS 和其他 Owncast 界面的相同全局窗口中运行。 三个含义:

  • 顶级 varfunction 声明落到 window 上。 Wrap your script in an IIFE ((function(){ ... })()) so private state stays private and you don't collide with the admin's JS or other plugins.
  • 主机对每个插件的贡献进行单独的 try/catch 包裹,因此运行时错误会抛到浏览器控制台(前缀为 owncast plugin \<your-slug> script error:),而不会停止其他插件的脚本。 语法错误不是隔离的:它会破坏一个连接的脚本标签的解析,在任意 try/catch 运行之前,因此请发布有效的 JavaScript。
  • 相对 fetch('./data.json') 解析针对查看器页面的 URL,而不是针对你的插件。 对于在 public/ 中打包的文件,请使用绝对路径,如 /plugins/\<your-slug>/data.json

动态脚本:onPageScripts

onPageStyles 的脚本对应。 从 onPageScripts 处理程序返回请求时计算的 JavaScript,且没有清单字段。 The host calls it once per /api/config for any plugin holding ui.modify that exports it, and appends the result to /customjavascript after the static manifest.scripts files, wrapped in the same per-plugin try/catch.

这适用于任何请求时的 JavaScript,而不仅仅是主题。 用来运行在查看器侧计算的代码,例如显示在插件的 KV 存储中管理员设置的值。 下面的示例向查看者显示该值:

module.exports = definePlugin({
// Run request-time JavaScript on the viewer page.
onPageScripts() {
const notice = owncast.kv.get('notice');
if (!notice) return;
return `alert(${JSON.stringify(notice)});`;
},
});

输出在共享的查看器 window 中运行,因此上述 IIFE 和绝对路径建议仍然适用。 转义你嵌入的任何不受信任的字符串:JavaScript 中的 JSON.stringify 和 Python 中的 json.dumps 都生成安全的引号文字,这就是示例在将其传递给 alert 之前将通知包裹在一个引号中。 Like the styles examples, reading the KV store requires storage.kv on top of ui.modify. 返回空(简单的 return,与返回 "" 相同)以贡献空内容。

何时使用它

scripts 是需要对查看器侧状态做出反应、在页面上自己挂载 UI,或与插件在 /plugins/\<your-slug>/ 上运行的后端进行通信的插件的正确工具。 对于基于聊天的机器人、消息过滤器和任何应该在服务器端运行的逻辑,常规插件处理程序更合适。 它们在主机沙箱内部运行,可以与查看器页面无法访问的 Owncast API 进行通信,并且不信任用户控制的 DOM。

额外页面内容

插件可以将 HTML 预置到查看器页面的额外内容块。 声明 manifest.extraPageContent 为一个对象,包含一个必需的 slug 和一个可选的 content 路径:

{
"permissions": ["ui.modify"],
"extraPageContent": { "slug": "banner", "content": "content.html" }
}
字段备注
slug必需。 当主机请求渲染的 HTML 时,会传递给页面内容处理程序的稳定标识符。 小写字母、数字和连字符,以字母开头。
content可选。 相对路径到 assets/ 中的静态 HTML 文件。 如果存在,该文件的字节会被直接内联。 如果省略,则主机会调用你的页面内容处理程序。

静态与动态

当 HTML 对每个查看器相同时使用 content:公告条、赞助商横幅、好几个段落。 如果内容应根据查看者变化或基于实时数据,则省略 content,并实现 页面内容处理程序。 主机以请求的 slug 和查看者的身份调用处理程序,你的处理程序返回要渲染的 HTML 字符串:

module.exports = definePlugin({
onPageContent(ctx) {
if (ctx.slug === 'banner') {
const who = ctx.user ? `, ${ctx.user.displayName}` : '';
return `<div class="banner">Welcome${who}!</div>`;
}
return '';
},
});

请参见 处理程序:页面内容 获取负载形状。 查看者身份在查看者经过身份验证时存在,对于匿名查看者则不存在。

需要 ui.modifyhttp.serve 不是必需的:HTML 被内联到 /api/config 响应中,而不是作为 URL 提供。

字节位于额外内容块的顶部,高于管理员配置的任何段落。 Each contribution is wrapped with an <!-- plugin: <your-slug> ... --> comment for attribution. 多个插件的贡献按照主机加载它们的顺序堆叠。

路径规则

stylesscripts 相同,应用于单个 .html 条目。 插件每个文件。 如果希望有几个不同的块,从你运送的一个文件中链接或 \<iframe> 它们。

Markdown 与 HTML

管理员的额外页面内容在呈现之前会经过 Owncast 的 Markdown 处理器。 插件 HTML 不会这样:主机首先在管理员的内容上运行 Markdown 处理器,然后再预置你的原始字节。 标签、属性和内联脚本按书写内容传递。

这意味着插件 HTML 可以使用查看器页面接受的任何元素。 这也意味着错误的标签可能会破坏周围的界面,因此转义任何你嵌入的不受信任的字符串(用户名、获取的文本、任何不在你控制之中的内容)。

scripts 配对

extraPageContent 在与 scripts 配对时表现优异:将标记作为 HTML 发出,可以一眼审查,并通过查询你声明的元素来连接你的 JavaScript 交互。 主机在脚本运行之前加载 HTML,因此针对插件贡献元素的脚本 document.getElementById(...) 的目标工作没有计时技巧。

{
"permissions": ["ui.modify", "http.serve"],
"extraPageContent": { "slug": "panel", "content": "panel.html" },
"scripts": ["panel.js"]
}

一种模式,往往比从仅使用 scripts 的插件命令式构建相同的 DOM 读取更干净:

  • panel.html 声明结构、类和你可以直接理解的 ID,作为普通 HTML。
  • panel.css(在 styles 中声明)对其进行主题化。
  • panel.js 附加事件监听器、获取数据、改变状态。

何时应使用 HTML 加 JS 而非纯 JavaScript:任何具有非简单布局、ARIA 属性或期望从现有 DOM 启动的第三方小部件的内容。 对于仅在某些条件下(例如在获取后,或在用户操作后)构建其 UI 的插件,纯 scripts 仍然是合理的选择,这种情况下在初始渲染时不渲染任何内容是正确的行为。

extraPageContent 本身就足够时

独立的 extraPageContent 是公告条、赞助商横幅,以及任何不需要响应事件的块的最简单路径:它直接发送标记, 不需要脚本,并且可以在禁用 JavaScript 的查看器中生存。

查看器页面标签

Plugins can add tabs to the viewer page's tab row next to the built-in About and Followers tabs by declaring manifest.tabs as an object. Each object key is the tab's stable slug. Every value requires a title, and content is optional.

{
"permissions": ["ui.modify"],
"tabs": {
"music": { "title": "Music", "content": "music.html" },
"stream-info": { "title": "Stream Info" }
}
}
Part备注
object keyRequired stable slug passed to the tab-content handler. 小写字母、数字和连字符,以字母开头。
title必填。 显示在标签上的标签。 在插件的标签中必须唯一。
content可选。 相对于 assets/ 中的静态 HTML 文件的路径。 如果存在,该文件的字节将直接内联。 如果省略,则主机调用标签内容处理程序。

The host derives the tab slug from the object key. Do not add a slug member to the value. The host rejects arrays and tab values containing the legacy slug member.

需要 ui.modifyhttp.serve is not required: each static tab's HTML is read from assets/ and inlined into the tab body. For a dynamic tab, the host passes the object key to the tab-content handler as slug and inlines the returned HTML.

标签如何被渲染

主机会在 /api/config 上发出 pluginTabs[] 数组。 查看器页面将每个条目映射到一个标签,其主体是内联的 HTML,在沙箱 iframe 中渲染,注入基准样式表,因此纯 HTML 看起来是原生的,而不需要你自己的 CSS。

Plugin-contributed tabs on the viewer page, shown alongside the built-in About and Followers tabs

See Styling plugin UI for the baseline and the helper classes. Tabs from each plugin are appended after the built-ins in lexicographic slug order. Ordering between tabs from different plugins is unspecified. JSON object order is not significant. The React key combines the tab slug and title, so changing either value remounts that tab.

The tab object key

The object key is a stable name you control. The host passes it to your tab-content handler as slug, so one handler can serve multiple tabs without guessing which one was requested. It also appears in host logs and future API calls, so pick something clear, like "music" or "stream-info". You can change title freely unless your code depends on it. Changing the key is a breaking change if code depends on the existing slug.

动态标签内容

When a tab value has no content file, the host calls your tab-content handler to produce it. 当内容应根据查看器变化或提取实时数据时,实现它。 The host resolves every dynamic tab while building the viewer's /api/config payload, once per config request rather than on tab click, so keep the handler fast. It passes the tab's object key as slug with the viewer's identity, and expects the HTML string for the tab body:

module.exports = definePlugin({
onTabContent(ctx) {
// ctx = { slug, user? }
if (ctx.slug === 'stream-info') {
return '<h2>Stream info</h2><p>Live every weekday at 8pm UTC.</p>';
}
return '';
},
});

参见 处理程序:标签内容 以获取有效负载形状。 查看器身份在经过身份验证时存在,而在匿名查看器中缺失。

路径规则

extraPageContent 相同,在每个条目中应用:

  • "music.html" 这样的简单路径自动以你的插件命名空间为前缀。
  • 完全合格的 /plugins/\<your-slug>/... 路径可以通过。
  • 其他插件命名空间中的路径将被拒绝。
  • http(s):// URLs 会被拒绝。
  • 每个条目必须以 .html 结尾。

标签标题

title 字段在标签栏中逐字显示。 保持简短:长标题会被标签 UI 截断。 对长度没有模式约束,但超过约 16 个字符的内容在移动设备上无法清晰显示。

何时使用标签与 extraPageContent

  • extraPageContent:一块 HTML 块,位于标签行上方。 适合公告条、赞助商横幅、任何应该始终可见的内容。
  • tabs:查看器可以点击的专用面板。 适合不需要与聊天抢注意力的内容:音乐列表、事件日程、链接页面、希望查看者找到但不一定要首先看到的赞助部分。

Improve this page

See something missing or incorrect? Edit this page and improve the documentation for everyone.

Contributors to this documentation
Gabe KangasGabe Kangas
G
Gabe Kangas