From 716c6f3f2f8c351e3cfbd7bd745819d174cd353f Mon Sep 17 00:00:00 2001 From: felis Date: Tue, 21 Jul 2026 19:41:49 -0700 Subject: [PATCH] add configurable gift menu overlays Add tenant-scoped gift menu settings, catalog-backed triggers, infinite OBS rendering, and guard assets. Normalize legacy and protobuf gift values for blind-box, battery-tier, and transaction-aware matching. --- README.md | 12 +- apps/overlay/README.md | 31 +- apps/overlay/public/assets/admiral.png | Bin 0 -> 12547 bytes apps/overlay/public/assets/captain.png | Bin 0 -> 10002 bytes apps/overlay/public/assets/governor.png | Bin 0 -> 14787 bytes apps/overlay/src/api.ts | 89 +++ apps/overlay/src/control.css | 147 +++++ apps/overlay/src/control.tsx | 610 +++++++++++++++++- apps/overlay/src/giftMenu.css | 389 +++++++++++ apps/overlay/src/giftMenu.tsx | 306 +++++++++ apps/overlay/src/giftMenuThemes.ts | 64 ++ apps/overlay/src/main.tsx | 3 + apps/overlay/src/types.ts | 56 +- apps/server-rust/README.md | 1 + apps/server-rust/migrations/010_gift_menu.sql | 6 + apps/server-rust/src/app.rs | 11 +- apps/server-rust/src/auth.rs | 18 + apps/server-rust/src/components.rs | 22 + apps/server-rust/src/domain.rs | 3 + apps/server-rust/src/gift_menu.rs | 510 +++++++++++++++ apps/server-rust/src/http_api.rs | 73 ++- apps/server-rust/src/lib.rs | 1 + apps/server-rust/src/live/bilibili.rs | 251 ++++++- apps/server-rust/src/overlay.rs | 76 ++- apps/server-rust/src/repository.rs | 24 +- docs/components/README.md | 7 +- docs/components/gift-menu.md | 33 + docs/protocol.md | 4 + resources/i18n.toml | 108 ++++ 29 files changed, 2764 insertions(+), 91 deletions(-) create mode 100644 apps/overlay/public/assets/admiral.png create mode 100644 apps/overlay/public/assets/captain.png create mode 100644 apps/overlay/public/assets/governor.png create mode 100644 apps/overlay/src/giftMenu.css create mode 100644 apps/overlay/src/giftMenu.tsx create mode 100644 apps/overlay/src/giftMenuThemes.ts create mode 100644 apps/server-rust/migrations/010_gift_menu.sql create mode 100644 apps/server-rust/src/gift_menu.rs create mode 100644 docs/components/gift-menu.md diff --git a/README.md b/README.md index f69b131..6873e78 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # 洛星瓷直播组件服务 -这是一个多用户 Rust/Axum 直播组件后端。目前内建 `danmaku_overlay` 弹幕姬、`song_request` 点歌姬与 -`gift_effect` -全屏礼物特效,后端已经按“直播源 → 强类型事件 → 组件实例”的方式拆分。镜像构建阶段会编译 React 前端,运行时由 Rust 同域托管控制台和 OBS 页面。 +这是一个多用户 Rust/Axum 直播组件后端。目前内建 `danmaku_overlay` 弹幕姬、`song_request` 点歌姬、 +`gift_effect` 全屏礼物特效与 `gift_menu` +礼物菜单,后端已经按“直播源 → 强类型事件 → 组件实例”的方式拆分。镜像构建阶段会编译 React 前端,运行时由 Rust 同域托管控制台和 OBS 页面。 直播连接使用相邻目录中的 [`libilibili`](https://github.com/feliscafra/libilibili) crate。它负责 Cookie/WBI @@ -21,6 +21,7 @@ adapter 只负责把强类型 Bilibili 命令转换成稳定的领域事件。cr - [`danmaku_overlay` 弹幕姬](docs/components/danmaku-overlay.md) - [`song_request` 点歌姬](docs/components/song-request.md) - [`gift_effect` 全屏礼物特效](docs/components/gift-effect.md) +- [`gift_menu` 礼物菜单](docs/components/gift-menu.md) - [WebSocket 实时协议](docs/protocol.md) - [租户、Secret 与部署安全](docs/security.md) - [完整配置注释](config.toml.example) @@ -210,12 +211,15 @@ fragment,不会随最初的 HTTP 请求发送到 Nginx;OBS 页面随后通 `/api/v1/components//stream` 完成认证。令牌只带 `events:subscribe` 权限,不能调用管理或写入接口;轮换后旧令牌立即失效。 -每个账户会自动拥有不可删除的点歌姬和全屏礼物特效组件。观众发送 `点歌 歌名` 加入队列,发送 `打分 1-5` +每个账户会自动拥有不可删除的点歌姬、全屏礼物特效和礼物菜单组件。观众发送 `点歌 歌名` 加入队列,发送 +`打分 1-5` 为当前歌曲评分;主播可从组件设置打开独立统计窗口,置顶、完成或取消队列项。点歌状态和评分持久化在 PostgreSQL,即使 OBS 未连接也不会丢失。 礼物特效组件在透明全屏浏览器源中展示从左向右飞行的礼物流星,并按礼物原始价值选择数量、尺寸和速度;舰长、提督和总督事件会临时覆盖一层不透明星河庆祝画面。所有档位参数、拖尾、星数、持续时间和低性能模式均可在控制台调整。该组件只订阅一次性 `live.gift` 与 `live.guard.buy`,不会把连击更新重复播放为新礼物。 +礼物菜单读取账户当前直播间的礼物目录与图标,可把指定礼物、舰长/提督/总督或指定礼物单价映射为自定义直播内容。OBS 以横排行无限循环展示,实际投喂命中时自动定位、暂停并播放渐变星花高亮。 + 旧格式 `/obs?token=...` 与 `/ws?token=...` 已移除,避免 bearer token 进入 Nginx 访问日志。升级后请在控制台为组件轮换令牌,并把旧 OBS 源替换为上述新地址;旧令牌一旦轮换便立即失效。 diff --git a/apps/overlay/README.md b/apps/overlay/README.md index 56534c6..31e5deb 100644 --- a/apps/overlay/README.md +++ b/apps/overlay/README.md @@ -18,20 +18,22 @@ Vite 在 Docker build stage 编译本目录,最终静态文件由 Rust 同域 ## 文件职责 -| 文件 | 职责 | -| --------------------- | ---------------------------------------------------- | -| `src/api.ts` | same-origin fetch、错误模型和兼容性 normalizer | -| `src/auth.tsx` | passwordless login、TOTP QR 与恢复码 | -| `src/control.tsx` | tenant component studio 和 system-admin 邀请码页面 | -| `src/stream.ts` | 通用组件 WebSocket 鉴权、重连和 renderer 分流 | -| `src/overlay.tsx` | 弹幕、礼物/表情和 OBS 自适应渲染 | -| `src/songOverlay.tsx` | 点歌快照 reducer、revision 校验与往返滚动 | -| `src/giftEffect.tsx` | 礼物流星、大航海全屏庆祝与视口自适应渲染 | -| `src/giftThemes.ts` | 可扩展礼物特效主题注册表与 CSS 变量 | -| `src/pwa.tsx` | install prompt、离线状态、显式更新和敏感状态 blocker | -| `src/i18n.tsx` | TOML 语言资源、浏览器回退和运行时切换 | -| `src/types.ts` | sanitized API view model 与 overlay settings | -| `pwa/control-sw.js` | `/control/` 静态壳层的缓存策略 | +| 文件 | 职责 | +| ----------------------- | ---------------------------------------------------- | +| `src/api.ts` | same-origin fetch、错误模型和兼容性 normalizer | +| `src/auth.tsx` | passwordless login、TOTP QR 与恢复码 | +| `src/control.tsx` | tenant component studio 和 system-admin 邀请码页面 | +| `src/stream.ts` | 通用组件 WebSocket 鉴权、重连和 renderer 分流 | +| `src/overlay.tsx` | 弹幕、礼物/表情和 OBS 自适应渲染 | +| `src/songOverlay.tsx` | 点歌快照 reducer、revision 校验与往返滚动 | +| `src/giftEffect.tsx` | 礼物流星、大航海全屏庆祝与视口自适应渲染 | +| `src/giftThemes.ts` | 可扩展礼物特效主题注册表与 CSS 变量 | +| `src/giftMenu.tsx` | 礼物菜单无限循环、触发定位与高亮 reducer | +| `src/giftMenuThemes.ts` | 可扩展礼物菜单主题注册表 | +| `src/pwa.tsx` | install prompt、离线状态、显式更新和敏感状态 blocker | +| `src/i18n.tsx` | TOML 语言资源、浏览器回退和运行时切换 | +| `src/types.ts` | sanitized API view model 与 overlay settings | +| `pwa/control-sw.js` | `/control/` 静态壳层的缓存策略 | ## Secret 与状态 @@ -73,3 +75,4 @@ YAML 和项目文档。 - [弹幕姬组件](../../docs/components/danmaku-overlay.md) - [点歌姬组件](../../docs/components/song-request.md) - [全屏礼物特效](../../docs/components/gift-effect.md) +- [礼物菜单组件](../../docs/components/gift-menu.md) diff --git a/apps/overlay/public/assets/admiral.png b/apps/overlay/public/assets/admiral.png new file mode 100644 index 0000000000000000000000000000000000000000..5025e7bb4cf11b7bd047031d0686f6146479de08 GIT binary patch literal 12547 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 W`%fJYB{DEDFnGH9xvX%NM literal 0 HcmV?d00001 diff --git a/apps/overlay/public/assets/captain.png b/apps/overlay/public/assets/captain.png new file mode 100644 index 0000000000000000000000000000000000000000..f3fac9b93d0c1e61a5ae61d0addc0a4758cad3bb GIT binary patch literal 10002 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 Vmj7W#85kHCJYD@<);T3K0RSLjnM42p literal 0 HcmV?d00001 diff --git a/apps/overlay/src/api.ts b/apps/overlay/src/api.ts index 2963efa..2b5f0ee 100644 --- a/apps/overlay/src/api.ts +++ b/apps/overlay/src/api.ts @@ -12,6 +12,9 @@ import type { ComponentSummary, CookieCloudSource, GiftEffectSettings, + GiftCatalogItem, + GiftMenuItem, + GiftMenuSettings, Invitation, OverlaySettings, Session, @@ -22,8 +25,10 @@ import type { } from './types' import { normalizeThemeId } from './themes' import { normalizeGiftEffectThemeId } from './giftThemes' +import { normalizeGiftMenuThemeId } from './giftMenuThemes' import { defaultGiftEffectSettings, + defaultGiftMenuSettings, defaultOverlaySettings, defaultSongRequestSettings, } from './types' @@ -236,6 +241,90 @@ export function normalizeGiftEffectSettings(value: unknown): GiftEffectSettings } } +function normalizeGiftMenuItem(value: unknown): GiftMenuItem | undefined { + const item = object(value) + const trigger = object(item.trigger) + if (typeof item.id !== 'string' || typeof item.description !== 'string') return undefined + if (trigger.kind === 'gift') { + const giftId = Number(trigger.giftId) + if (!Number.isSafeInteger(giftId) || giftId <= 0 || typeof trigger.giftName !== 'string') + return undefined + return { + id: item.id, + description: item.description, + trigger: { + kind: 'gift', + giftId, + giftName: trigger.giftName, + unitPrice: Number(trigger.unitPrice ?? 0), + imageUrl: typeof trigger.imageUrl === 'string' ? trigger.imageUrl : undefined, + }, + } + } + if ( + trigger.kind === 'guard' && + ['captain', 'admiral', 'governor'].includes(String(trigger.level)) + ) { + return { + id: item.id, + description: item.description, + trigger: { + kind: 'guard', + level: trigger.level as 'captain' | 'admiral' | 'governor', + }, + } + } + const amount = Number(trigger.amount) + if (trigger.kind === 'battery' && Number.isSafeInteger(amount) && amount > 0) { + return { + id: item.id, + description: item.description, + trigger: { kind: 'battery', amount }, + } + } + return undefined +} + +export function normalizeGiftMenuSettings(value: unknown): GiftMenuSettings { + const root = object(value) + const settings = object(root.settings ?? value) + const items = Array.isArray(settings.items) + ? settings.items.map(normalizeGiftMenuItem).filter(item => item !== undefined) + : [] + return { + ...defaultGiftMenuSettings, + ...(settings as Partial), + themeId: normalizeGiftMenuThemeId(settings.themeId), + items, + } +} + +export function normalizeGiftCatalog(value: unknown): GiftCatalogItem[] { + const root = object(value) + if (!Array.isArray(root.gifts)) return [] + return root.gifts.flatMap(candidate => { + const gift = object(candidate) + const id = Number(gift.id) + if (!Number.isSafeInteger(id) || id <= 0 || typeof gift.name !== 'string') return [] + return [ + { + id, + name: gift.name, + coinType: String(gift.coinType ?? 'gold'), + batteryValue: Number(gift.batteryValue ?? Number(gift.unitPrice ?? 0) / 100), + unitPrice: Number(gift.unitPrice ?? 0), + imageUrl: + typeof gift.imageUrl === 'string' + ? gift.imageUrl + : typeof gift.animationUrl === 'string' + ? gift.animationUrl + : undefined, + animationUrl: typeof gift.animationUrl === 'string' ? gift.animationUrl : undefined, + }, + ] + }) +} + export function normalizeSongRequestItem(value: unknown): SongRequestItem | undefined { const item = object(value) const requester = object(item.requester) diff --git a/apps/overlay/src/control.css b/apps/overlay/src/control.css index f9719d6..69262d8 100644 --- a/apps/overlay/src/control.css +++ b/apps/overlay/src/control.css @@ -217,6 +217,153 @@ } } +.gift-menu-settings-editor { + gap: 22px; +} + +.gift-catalog-status { + display: grid; + align-content: start; + justify-items: start; + gap: 7px; + padding: 14px; + border: 1px solid rgba(102, 229, 211, 0.2); + border-radius: 14px; + background: rgba(2, 31, 43, 0.48); +} + +.gift-catalog-status span { + color: var(--muted); +} + +.gift-menu-builder { + display: grid; + grid-template-columns: + minmax(140px, 0.7fr) minmax(190px, 1fr) minmax(220px, 1.4fr) + auto; + align-items: end; + gap: 12px; +} + +.gift-menu-builder legend { + padding: 0 8px; + color: var(--accent); + font-weight: 700; +} + +.gift-menu-builder label { + display: grid; + gap: 7px; +} + +.gift-menu-item-editor-list { + display: grid; + gap: 8px; +} + +.gift-menu-item-editor { + display: grid; + min-width: 0; + grid-template-columns: 34px 52px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid rgba(101, 226, 211, 0.2); + border-radius: 13px; + background: linear-gradient(105deg, rgba(4, 36, 49, 0.72), rgba(8, 63, 65, 0.42)); +} + +.gift-menu-item-editor > img, +.gift-menu-editor-mark { + display: grid; + width: 46px; + height: 46px; + place-items: center; + object-fit: contain; + border: 1px solid rgba(255, 224, 138, 0.3); + border-radius: 50%; + background: rgba(3, 25, 37, 0.7); +} + +.gift-menu-editor-index { + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +.gift-menu-item-editor > div:not(.gift-menu-item-actions) { + display: grid; + min-width: 0; + gap: 3px; +} + +.gift-menu-item-editor > div span { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.gift-menu-item-actions { + display: flex; + gap: 6px; +} + +.gift-menu-item-actions button { + min-width: 38px; + padding: 7px 10px; +} + +.gift-menu-renderer-settings { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.gift-menu-preview-viewport { + width: 100%; + height: clamp(260px, 38vw, 460px); + overflow: hidden; + border: 1px dashed rgba(111, 240, 216, 0.55); + border-radius: 15px; + background-color: #02101a; + background-image: + linear-gradient(45deg, rgba(72, 161, 158, 0.08) 25%, transparent 25%), + linear-gradient(-45deg, rgba(72, 161, 158, 0.08) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(72, 161, 158, 0.08) 75%), + linear-gradient(-45deg, transparent 75%, rgba(72, 161, 158, 0.08) 75%); + background-position: + 0 0, + 0 12px, + 12px -12px, + -12px 0; + background-size: 24px 24px; +} + +@media (max-width: 900px) { + .gift-menu-builder, + .gift-menu-renderer-settings { + grid-template-columns: 1fr 1fr; + } + + .gift-menu-description-field { + grid-column: 1 / -1; + } +} + +@media (max-width: 620px) { + .gift-menu-builder, + .gift-menu-renderer-settings { + grid-template-columns: 1fr; + } + + .gift-menu-item-editor { + grid-template-columns: 28px 46px minmax(0, 1fr); + } + + .gift-menu-item-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } +} + html, body, #root, diff --git a/apps/overlay/src/control.tsx b/apps/overlay/src/control.tsx index c9de582..e5d726b 100644 --- a/apps/overlay/src/control.tsx +++ b/apps/overlay/src/control.tsx @@ -15,7 +15,9 @@ import { errorMessage, json, normalizeComponents, + normalizeGiftCatalog, normalizeGiftEffectSettings, + normalizeGiftMenuSettings, normalizeInvitations, normalizeSettings, normalizeSongRequestPage, @@ -24,14 +26,17 @@ import { } from './api' import { Overlay } from './overlay' import { GiftEffectOverlay } from './giftEffect' +import { GiftMenuOverlay } from './giftMenu' import type { GiftEffectPreviewMode } from './giftEffect' import { getGiftEffectTheme, giftEffectThemes } from './giftThemes' +import { getGiftMenuTheme, giftMenuGuardIconUrl, giftMenuThemes } from './giftMenuThemes' import { PwaControls, usePwaUpdateBlocker } from './pwa' import { SongRequestOverlay } from './songOverlay' import { getOverlayTheme, overlayThemes } from './themes' import { currentLanguage, LanguageSelect, translate, useI18n } from './i18n' import { defaultGiftEffectSettings, + defaultGiftMenuSettings, defaultOverlaySettings, defaultSongRequestSettings, } from './types' @@ -41,6 +46,9 @@ import type { ComponentSummary, CookieCloudSource, GiftEffectSettings, + GiftCatalogItem, + GiftMenuItem, + GiftMenuSettings, Invitation, OverlaySettings, SongRequestItem, @@ -51,7 +59,12 @@ import type { const previewPresets = [ { id: 'narrow', labelKey: 'preview.narrow', width: 360, height: 600 }, { id: 'portrait', labelKey: 'preview.portrait', width: 440, height: 760 }, - { id: 'hd-portrait', labelKey: 'preview.hd_portrait', width: 600, height: 1080 }, + { + id: 'hd-portrait', + labelKey: 'preview.hd_portrait', + width: 600, + height: 1080, + }, { id: 'horizontal', labelKey: 'preview.horizontal', width: 720, height: 320 }, ] @@ -67,6 +80,10 @@ function isGiftEffectKind(kind: string): boolean { return kind === 'gift_effect' } +function isGiftMenuKind(kind: string): boolean { + return kind === 'gift_menu' +} + type Flash = { kind: 'success' | 'error'; text: string } | undefined function Panel({ @@ -749,6 +766,423 @@ function GiftEffectPreview({ settings }: { settings: GiftEffectSettings }) { ) } +function GiftMenuSettingsEditor({ + componentId, + settings, + onChange, + onSave, + saving, +}: { + componentId: string + settings: GiftMenuSettings + onChange: (settings: GiftMenuSettings) => void + onSave: () => Promise + saving: boolean +}) { + const [catalog, setCatalog] = useState([]) + const [catalogBusy, setCatalogBusy] = useState(false) + const [catalogError, setCatalogError] = useState('') + const [triggerKind, setTriggerKind] = useState<'gift' | 'guard' | 'battery'>('gift') + const [giftId, setGiftId] = useState('') + const [guardLevel, setGuardLevel] = useState<'captain' | 'admiral' | 'governor'>('captain') + const [battery, setBattery] = useState(150) + const [description, setDescription] = useState('') + const [draftError, setDraftError] = useState('') + const theme = getGiftMenuTheme(settings.themeId) + const edit = (key: K, value: GiftMenuSettings[K]) => + onChange({ ...settings, [key]: value }) + + const loadCatalog = useCallback( + async (refresh: boolean) => { + setCatalogBusy(true) + setCatalogError('') + try { + const path = `/api/v1/components/${encodeURIComponent(componentId)}/gift-catalog${refresh ? '/refresh' : ''}` + const payload = await api(path, refresh ? json('POST') : undefined) + const gifts = normalizeGiftCatalog(payload) + setCatalog(gifts) + setGiftId(current => current || String(gifts[0]?.id ?? '')) + } catch (reason) { + setCatalogError(errorMessage(reason, translate('gift_menu.catalog.failed'))) + } finally { + setCatalogBusy(false) + } + }, + [componentId], + ) + + useEffect(() => { + void loadCatalog(false) + }, [loadCatalog]) + + const addItem = () => { + setDraftError('') + const explanation = description.trim() + if (!explanation) { + setDraftError(translate('gift_menu.editor.description_required')) + return + } + let item: GiftMenuItem | undefined + if (triggerKind === 'gift') { + const gift = catalog.find(candidate => candidate.id === Number(giftId)) + if (!gift) { + setDraftError(translate('gift_menu.editor.gift_required')) + return + } + item = { + id: crypto.randomUUID(), + description: explanation, + trigger: { + kind: 'gift', + giftId: gift.id, + giftName: gift.name, + imageUrl: gift.imageUrl, + unitPrice: gift.unitPrice, + }, + } + } else if (triggerKind === 'guard') { + item = { + id: crypto.randomUUID(), + description: explanation, + trigger: { kind: 'guard', level: guardLevel }, + } + } else { + item = { + id: crypto.randomUUID(), + description: explanation, + trigger: { kind: 'battery', amount: Math.max(1, Math.floor(battery)) }, + } + } + const key = JSON.stringify(item.trigger) + if (settings.items.some(existing => JSON.stringify(existing.trigger) === key)) { + setDraftError(translate('gift_menu.editor.duplicate')) + return + } + edit('items', [...settings.items, item]) + setDescription('') + } + + const move = (index: number, direction: -1 | 1) => { + const target = index + direction + if (target < 0 || target >= settings.items.length) return + const items = [...settings.items] + ;[items[index], items[target]] = [items[target], items[index]] + edit('items', items) + } + + return ( +

+
+ +
+ {translate('gift_menu.catalog.title')} + + {catalogBusy + ? translate('gift_menu.catalog.loading') + : translate('gift_menu.catalog.count', { count: catalog.length })} + + +
+
+ {catalogError &&
{catalogError}
} + +
+ {translate('gift_menu.editor.add_title')} + + {triggerKind === 'gift' && ( + + )} + {triggerKind === 'guard' && ( + + )} + {triggerKind === 'battery' && ( + + )} + + +
+ {draftError &&
{draftError}
} + +
+ {settings.items.length === 0 ? ( +
{translate('gift_menu.editor.empty')}
+ ) : ( + settings.items.map((item, index) => ( +
+ {index + 1} + {item.trigger.kind === 'gift' && item.trigger.imageUrl ? ( + + ) : item.trigger.kind === 'guard' ? ( + + ) : ( + + {item.trigger.kind === 'battery' + ? translate('gift_menu.battery_mark') + : translate('components.gift_mark')} + + )} +
+ + {item.trigger.kind === 'gift' + ? item.trigger.giftName + : item.trigger.kind === 'guard' + ? translate(`gift_menu.guard.${item.trigger.level}`) + : translate('gift_menu.overlay.battery', { + amount: item.trigger.amount, + })} + + {item.description} +
+
+ + + +
+
+ )) + )} +
+ +
+ + + + + + +
+
+ +
+
+ +
+
+ ) +} + +function GiftMenuPreview({ settings }: { settings: GiftMenuSettings }) { + const [triggered, setTriggered] = useState() + const [nonce, setNonce] = useState(0) + const trigger = (id: string) => { + setTriggered(id) + setNonce(current => current + 1) + } + return ( + +
+ +
+
+ ) +} + function SourceEditor({ source, onSaved, @@ -797,7 +1231,10 @@ function SourceEditor({ setPassword('') setFlash({ kind: 'success', text: translate('source.saved') }) } catch (reason) { - setFlash({ kind: 'error', text: errorMessage(reason, translate('source.save_failed')) }) + setFlash({ + kind: 'error', + text: errorMessage(reason, translate('source.save_failed')), + }) } finally { setBusy(false) } @@ -934,7 +1371,10 @@ function ObsAccessPanel({ component }: { component: ComponentSummary }) { .catch(reason => { if (cancelled) return setState({ publicId: component.publicId, configured: false }) - setFlash({ kind: 'error', text: errorMessage(reason, translate('obs.status_failed')) }) + setFlash({ + kind: 'error', + text: errorMessage(reason, translate('obs.status_failed')), + }) }) return () => { cancelled = true @@ -957,7 +1397,10 @@ function ObsAccessPanel({ component }: { component: ComponentSummary }) { text: translate('obs.token_created'), }) } catch (reason) { - setFlash({ kind: 'error', text: errorMessage(reason, translate('obs.rotate_failed')) }) + setFlash({ + kind: 'error', + text: errorMessage(reason, translate('obs.rotate_failed')), + }) } finally { setBusy(false) } @@ -1035,6 +1478,7 @@ function TestEvents({ const [name, setName] = useState(() => translate('test.default_viewer')) const [text, setText] = useState(() => translate('test.default_text')) const [giftName, setGiftName] = useState(() => translate('test.default_gift')) + const [giftId, setGiftId] = useState('') const [quantity, setQuantity] = useState(1) const [battery, setBattery] = useState(100) const [guardName, setGuardName] = useState(() => translate('test.default_guard')) @@ -1054,13 +1498,23 @@ function TestEvents({ uid, name, ...(kind === 'danmaku' ? { text } : {}), - ...(kind === 'gift' ? { giftName, quantity, battery } : {}), + ...(kind === 'gift' + ? { + giftName, + quantity, + battery, + ...(giftId ? { giftId: Number(giftId) } : {}), + } + : {}), ...(kind === 'guard' ? { guardName, quantity, price: guardPrice } : {}), }), ) setFlash({ kind: 'success', text: translate('test.sent') }) } catch (reason) { - setFlash({ kind: 'error', text: errorMessage(reason, translate('test.failed')) }) + setFlash({ + kind: 'error', + text: errorMessage(reason, translate('test.failed')), + }) } finally { setBusy(false) } @@ -1102,6 +1556,15 @@ function TestEvents({ onChange={event => setGiftName(event.target.value)} /> +