新增自定义颜色池功能,CONVERT_COLOR为color时支持自定义颜色 (#243)
- 新增 COLOR_POOL 环境变量,支持自定义弹幕随机颜色池 - 前端新增色轮选色器、随机添加、批量添加弹窗(支持十进制和#hex格式) - 颜色池样式提取到 components.css.js,JS 中仅保留动态样式 - 不配置时使用默认颜色池,行为与原来一致 - 同步更新 forward-widget、README、.env.example Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e1487409d
commit
b267db281d
7 files changed
+548
-18
No files matched your search
@@ -74,7 +74,7 @@ LogVar 弹幕 API 服务器
|
||||
- **手动搜索支持输入播放链接获取弹幕**:支持手动搜索的播放器输入爱优腾芒哔咪狐乐西播放链接可获取弹幕,如`senplayer`。
|
||||
- **弹幕转换功能**:支持通过环境变量配置弹幕转换规则,包括:
|
||||
- 将顶部和底部弹幕转换为浮动弹幕(`CONVERT_TOP_BOTTOM_TO_SCROLL`)
|
||||
- 转换弹幕颜色为白色或彩色(`CONVERT_COLOR`)
|
||||
- 转换弹幕颜色为白色或彩色(`CONVERT_COLOR`),支持自定义颜色池(`COLOR_POOL`)
|
||||
- 解决部分播放器不支持顶部/底部弹幕和彩色弹幕的问题
|
||||
- 增加点赞数显示,先去重再拼接点赞标记,点赞数缩写显示,≥5 才显示,避免低赞干扰
|
||||
- **弹幕限制数量**:支持通过环境变量配置等间隔采样弹幕数量。
|
||||
@@ -398,6 +398,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
|
||||
| DANMU_LIMIT | 【可选】等间隔采样限制弹幕总数,单位为k,即千:默认 0,表示不限制弹幕数,若改为5,弹幕总数在超过5000的情况下会将弹幕数控制在5000 |
|
||||
| CONVERT_TOP_BOTTOM_TO_SCROLL | 【可选】是否将顶部和底部弹幕转换为浮动弹幕,默认为`false`(不转换),启用后顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1),可选值:`true`、`false` |
|
||||
| CONVERT_COLOR | 【可选】弹幕转换颜色配置,默认为`default`(不转换),`white` 将所有非白色的弹幕颜色转换为纯白色,`color` 将所有白色弹幕转换为随机颜色(包含白色),可选值:`default`、`white`、`color` |
|
||||
| COLOR_POOL | 【可选】自定义颜色池(`CONVERT_COLOR`为`color`时生效),不配置使用默认颜色池(白、红、橙、黄、绿、青、蓝、紫、粉),格式:十进制颜色值逗号分隔,例如:`16711680,65280,255,16776960` |
|
||||
| LIKE_SWITCH | 【可选】弹幕点赞数显示开关,默认为`true`(开启),开启后会在弹幕内容后显示点赞数标记,≥5 才显示,避免低赞干扰 |
|
||||
| DANMU_OUTPUT_FORMAT | 【可选】弹幕输出格式,默认为`json`,可选值:`json`(JSON格式)、`xml`(XML格式),支持通过查询参数`?format=xml`或`?format=json`覆盖此设置,优先级:查询参数 > 环境变量 > 默认值 |
|
||||
| DANMU_SIMPLIFIED_TRADITIONAL | 【可选】弹幕简繁体转换设置:default(默认不转换)、simplified(繁转简)、traditional(简转繁) |
|
||||
|
||||
@@ -154,6 +154,12 @@ CONVERT_TOP_BOTTOM_TO_SCROLL=false
|
||||
# 可选值:default, white, color
|
||||
CONVERT_COLOR=default
|
||||
|
||||
# 自定义颜色池(CONVERT_COLOR为color时生效)
|
||||
# 默认值:空(使用内置默认颜色池:白、红、橙、黄、绿、青、蓝、紫、粉)
|
||||
# 说明:格式为十进制颜色值逗号分隔,例如 16711680 表示红色(即 #FF0000)
|
||||
# 示例:16711680,65280,255,16776960
|
||||
COLOR_POOL=
|
||||
|
||||
# 弹幕简繁体转换设置:default(默认不转换)、simplified(繁转简)、traditional(简转繁)
|
||||
# 默认值:default(不转换)
|
||||
# 可选值:default, simplified, traditional
|
||||
|
||||
@@ -524,6 +524,7 @@ export class Envs {
|
||||
'DANMU_SIMPLIFIED_TRADITIONAL': { category: 'danmu', type: 'select', options: ['default', 'simplified', 'traditional'], description: '弹幕简繁体转换设置:default(默认不转换)、simplified(繁转简)、traditional(简转繁)' },
|
||||
'CONVERT_TOP_BOTTOM_TO_SCROLL': { category: 'danmu', type: 'boolean', description: '顶部/底部弹幕转换为浮动弹幕' },
|
||||
'CONVERT_COLOR': { category: 'danmu', type: 'select', options: ['default', 'white', 'color'], description: '弹幕转换颜色配置' },
|
||||
'COLOR_POOL': { category: 'danmu', type: 'text', description: '自定义颜色池(CONVERT_COLOR为color时生效),不配置使用默认颜色池,格式:十进制颜色值逗号分隔' },
|
||||
'DANMU_OUTPUT_FORMAT': { category: 'danmu', type: 'select', options: ['json', 'xml'], description: '弹幕输出格式,默认json' },
|
||||
'DANMU_PUSH_URL': { category: 'danmu', type: 'text', description: '弹幕推送地址,示例 http://127.0.0.1:9978/action?do=refresh&type=danmaku&path= ' },
|
||||
'LIKE_SWITCH': { category: 'danmu', type: 'boolean', description: '弹幕点赞数显示开关,默认开启' },
|
||||
@@ -587,6 +588,7 @@ export class Envs {
|
||||
commentCacheMinutes: this.get('COMMENT_CACHE_MINUTES', 3, 'number'), // 弹幕缓存时间配置(分钟,默认 3)
|
||||
convertTopBottomToScroll: this.get('CONVERT_TOP_BOTTOM_TO_SCROLL', false, 'boolean'), // 顶部/底部弹幕转换为浮动弹幕配置(默认 false,禁用转换)
|
||||
convertColor: this.get('CONVERT_COLOR', 'default', 'string'), // 弹幕转换颜色配置,支持 default、white、color(默认 default,禁用转换)
|
||||
colorPool: this.get('COLOR_POOL', '16777215,16777215,16777215,16777215,16777215,16777215,16777215,16777215,16744319,16752762,16774799,9498256,8388564,8900346,14204888,16758465', 'string'), // 自定义颜色池,CONVERT_COLOR为color时生效
|
||||
danmuOutputFormat: this.get('DANMU_OUTPUT_FORMAT', 'json', 'string'), // 弹幕输出格式配置(默认 json,可选值:json, xml)
|
||||
strictTitleMatch: this.get('STRICT_TITLE_MATCH', false, 'boolean'), // 严格标题匹配模式配置(默认 false,宽松模糊匹配)
|
||||
titleToChinese: this.get('TITLE_TO_CHINESE', false, 'boolean'), // 外语标题转换中文开关
|
||||
|
||||
@@ -1453,6 +1453,218 @@ export const componentsCssContent = /* css */ `
|
||||
}
|
||||
}
|
||||
|
||||
/* 颜色池配置 */
|
||||
.color-pool-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
min-height: 36px;
|
||||
padding: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.color-pool-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.color-pool-swatch {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.color-pool-value {
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.color-pool-remove {
|
||||
border: none;
|
||||
background: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.color-pool-empty {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.color-pool-picker {
|
||||
padding: 14px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.color-pool-picker-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.color-wheel {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(hsl(0,100%,50%),hsl(60,100%,50%),hsl(120,100%,50%),hsl(180,100%,50%),hsl(240,100%,50%),hsl(300,100%,50%),hsl(360,100%,50%));
|
||||
cursor: crosshair;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-wheel-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
width: 44%;
|
||||
height: 44%;
|
||||
border-radius: 50%;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.color-wheel-dot {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2.5px solid #fff;
|
||||
box-shadow: 0 0 4px rgba(0,0,0,0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-pool-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.color-pool-preview-swatch {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-pool-preview-hex {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.color-pool-lightness {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.color-pool-lightness span {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-pool-lightness input {
|
||||
flex: 1;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.color-pool-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.color-pool-actions .btn {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-pool-actions .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 批量添加颜色弹窗 */
|
||||
.batch-color-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.4);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.batch-color-dialog {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.batch-color-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-color-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.batch-color-preview-swatch {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.batch-color-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* 偏移规则快速配置 */
|
||||
.offset-rule-panel {
|
||||
display: none;
|
||||
|
||||
@@ -566,10 +566,48 @@ function renderValueInput(item) {
|
||||
const currentKey = document.getElementById('env-key') ? document.getElementById('env-key').value : '';
|
||||
const isBilibiliCookie = currentKey === 'BILIBILI_COOKIE';
|
||||
const isAiApiKey = currentKey === 'AI_API_KEY';
|
||||
const isColorPool = currentKey === 'COLOR_POOL';
|
||||
const isDanmuOffset = currentKey === 'DANMU_OFFSET';
|
||||
const offsetSources = item && item.sources ? item.sources : [];
|
||||
|
||||
if (isDanmuOffset) {
|
||||
if (isColorPool) {
|
||||
// 自定义颜色池专用编辑界面
|
||||
const colors = parseColorPool(value);
|
||||
|
||||
container.innerHTML = \`
|
||||
<label>颜色池配置 (CONVERT_COLOR 为 color 时生效)</label>
|
||||
<div id="color-pool-display" class="color-pool-display">
|
||||
\${renderColorItems(colors)}
|
||||
</div>
|
||||
<div class="color-pool-picker">
|
||||
<div class="color-pool-picker-inner">
|
||||
<div id="color-wheel" class="color-wheel">
|
||||
<div class="color-wheel-center"></div>
|
||||
<div id="wheel-dot" class="color-wheel-dot" style="top: 2px; left: 53px; background: hsl(0,100%,50%);"></div>
|
||||
</div>
|
||||
<div class="color-pool-preview">
|
||||
<div id="color-preview-swatch" class="color-pool-preview-swatch" style="background: #ff0000;"></div>
|
||||
<span id="color-preview-hex" class="color-pool-preview-hex">#ff0000</span>
|
||||
</div>
|
||||
<div class="color-pool-lightness">
|
||||
<span>亮度</span>
|
||||
<input type="range" id="color-lightness" min="10" max="100" value="50">
|
||||
</div>
|
||||
<div class="color-pool-actions">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addColorToPool()">添加到颜色池</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addRandomColorToPool()">随机添加</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-pool-actions">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="showBatchColorDialog()">批量添加</button>
|
||||
<div class="spacer"></div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="resetColorPool()">恢复默认</button>
|
||||
</div>
|
||||
<textarea id="text-value" style="display: none;">\${value}</textarea>
|
||||
\`;
|
||||
setTimeout(initColorWheel, 0);
|
||||
} else if (isDanmuOffset) {
|
||||
// DANMU_OFFSET 专用编辑界面
|
||||
const rows = value && value.length > 50 ? Math.min(Math.max(Math.ceil(value.length / 50), 3), 10) : 3;
|
||||
container.innerHTML = \`
|
||||
@@ -689,6 +727,258 @@ function renderValueInput(item) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 颜色池操作函数 =====
|
||||
|
||||
// 色轮状态
|
||||
let wheelHue = 0;
|
||||
let wheelLightness = 50;
|
||||
let wheelCleanup = null;
|
||||
|
||||
// 解析颜色池字符串为十进制数组
|
||||
function parseColorPool(str) {
|
||||
if (!str) return [];
|
||||
return str.split(',').map(c => parseInt(c.trim(), 10)).filter(c => !isNaN(c) && c >= 0 && c <= 16777215);
|
||||
}
|
||||
|
||||
// 渲染颜色池色块 HTML
|
||||
function renderColorItems(colors) {
|
||||
if (colors.length === 0) return '<span class="color-pool-empty">未配置,将使用默认颜色池</span>';
|
||||
return colors.map((c, i) => \`
|
||||
<div class="color-pool-item">
|
||||
<span class="color-pool-swatch" style="background: #\${c.toString(16).padStart(6, '0')};"></span>
|
||||
<span class="color-pool-value">\${c}</span>
|
||||
<button type="button" class="color-pool-remove" onclick="removeColorFromPool(\${i})">×</button>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// HSL -> RGB -> 十进制
|
||||
function hslToDecimal(h, s, l) {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
const k = n => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
||||
const r = Math.round(f(0) * 255);
|
||||
const g = Math.round(f(8) * 255);
|
||||
const b = Math.round(f(4) * 255);
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// 更新色轮预览
|
||||
function updateWheelPreview() {
|
||||
const dec = hslToDecimal(wheelHue, 100, wheelLightness);
|
||||
const hex = '#' + dec.toString(16).padStart(6, '0');
|
||||
const swatch = document.getElementById('color-preview-swatch');
|
||||
const hexLabel = document.getElementById('color-preview-hex');
|
||||
const dot = document.getElementById('wheel-dot');
|
||||
if (swatch) swatch.style.background = hex;
|
||||
if (hexLabel) hexLabel.textContent = hex;
|
||||
if (dot) dot.style.background = hex;
|
||||
}
|
||||
|
||||
// 初始化色轮交互
|
||||
function initColorWheel() {
|
||||
if (wheelCleanup) wheelCleanup();
|
||||
|
||||
const wheel = document.getElementById('color-wheel');
|
||||
const slider = document.getElementById('color-lightness');
|
||||
if (!wheel) return;
|
||||
|
||||
let dragging = false;
|
||||
|
||||
function handleWheelEvent(e) {
|
||||
const rect = wheel.getBoundingClientRect();
|
||||
const cx = rect.width / 2;
|
||||
const cy = rect.height / 2;
|
||||
const x = e.clientX - rect.left - cx;
|
||||
const y = e.clientY - rect.top - cy;
|
||||
const dist = Math.sqrt(x * x + y * y);
|
||||
const outerR = rect.width / 2;
|
||||
const innerR = outerR * 0.22;
|
||||
if (dist < innerR || dist > outerR) return;
|
||||
let angle = Math.atan2(y, x) * 180 / Math.PI + 90;
|
||||
if (angle < 0) angle += 360;
|
||||
wheelHue = Math.round(angle % 360);
|
||||
const dot = document.getElementById('wheel-dot');
|
||||
if (dot) {
|
||||
const r = (innerR + outerR) / 2;
|
||||
const rad = (wheelHue - 90) * Math.PI / 180;
|
||||
dot.style.left = (cx + r * Math.cos(rad) - 7) + 'px';
|
||||
dot.style.top = (cy + r * Math.sin(rad) - 7) + 'px';
|
||||
}
|
||||
updateWheelPreview();
|
||||
}
|
||||
|
||||
const onMove = e => {
|
||||
if (dragging) handleWheelEvent(e);
|
||||
};
|
||||
const onTouchMove = e => {
|
||||
if (dragging) handleWheelEvent(e.touches[0]);
|
||||
};
|
||||
const onUp = () => {
|
||||
dragging = false;
|
||||
};
|
||||
|
||||
wheel.addEventListener('mousedown', e => {
|
||||
dragging = true;
|
||||
handleWheelEvent(e);
|
||||
});
|
||||
wheel.addEventListener('touchstart', e => {
|
||||
dragging = true;
|
||||
handleWheelEvent(e.touches[0]);
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('touchend', onUp);
|
||||
|
||||
if (slider) {
|
||||
slider.addEventListener('input', function() {
|
||||
wheelLightness = parseInt(this.value, 10);
|
||||
updateWheelPreview();
|
||||
});
|
||||
}
|
||||
|
||||
wheelCleanup = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
wheelCleanup = null;
|
||||
};
|
||||
|
||||
updateWheelPreview();
|
||||
}
|
||||
|
||||
// 颜色池 - 追加颜色值
|
||||
function appendColorToPool(decimal) {
|
||||
const textarea = document.getElementById('text-value');
|
||||
const current = textarea.value.trim();
|
||||
textarea.value = current ? current + ',' + decimal : String(decimal);
|
||||
syncColorPoolDisplay();
|
||||
}
|
||||
|
||||
// 颜色池 - 从色轮添加
|
||||
function addColorToPool() {
|
||||
appendColorToPool(hslToDecimal(wheelHue, 100, wheelLightness));
|
||||
}
|
||||
|
||||
// 颜色池 - 随机添加(crypto 真随机)
|
||||
function addRandomColorToPool() {
|
||||
const arr = new Uint32Array(1);
|
||||
crypto.getRandomValues(arr);
|
||||
appendColorToPool(arr[0] % 16777216);
|
||||
}
|
||||
|
||||
// 颜色池 - 删除指定颜色
|
||||
function removeColorFromPool(index) {
|
||||
const textarea = document.getElementById('text-value');
|
||||
const colors = textarea.value.split(',').map(c => c.trim()).filter(c => c);
|
||||
colors.splice(index, 1);
|
||||
textarea.value = colors.join(',');
|
||||
syncColorPoolDisplay();
|
||||
}
|
||||
|
||||
// 颜色池 - 恢复默认(清空值,后端默认值自动生效)
|
||||
function resetColorPool() {
|
||||
const textarea = document.getElementById('text-value');
|
||||
textarea.value = '';
|
||||
syncColorPoolDisplay();
|
||||
}
|
||||
|
||||
// 颜色池 - 同步色块展示
|
||||
function syncColorPoolDisplay() {
|
||||
const textarea = document.getElementById('text-value');
|
||||
const display = document.getElementById('color-pool-display');
|
||||
if (!textarea || !display) return;
|
||||
display.innerHTML = renderColorItems(parseColorPool(textarea.value));
|
||||
}
|
||||
|
||||
// 颜色池 - 批量添加弹窗
|
||||
function showBatchColorDialog() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'batch-color-overlay';
|
||||
overlay.className = 'batch-color-overlay';
|
||||
overlay.innerHTML = \`
|
||||
<div class="batch-color-dialog">
|
||||
<div style="font-size: 15px; font-weight: 600; margin-bottom: 12px;">批量添加颜色</div>
|
||||
<div class="form-help" style="margin: 0 0 10px 0;">支持十进制(如 16777215)和十六进制(如 #ffffff),逗号分隔</div>
|
||||
<textarea id="batch-color-input" class="batch-color-input" placeholder="例如: #ff0000, 65280, #0000ff, 16776960" rows="4"></textarea>
|
||||
<div id="batch-color-preview" class="batch-color-preview"></div>
|
||||
<div class="batch-color-actions">
|
||||
<button type="button" class="btn btn-sm" onclick="closeBatchColorDialog()">取消</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="confirmBatchColor()">确认添加</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) closeBatchColorDialog();
|
||||
});
|
||||
const input = document.getElementById('batch-color-input');
|
||||
if (input) input.addEventListener('input', updateBatchColorPreview);
|
||||
}
|
||||
|
||||
// 颜色池 - 关闭批量弹窗
|
||||
function closeBatchColorDialog() {
|
||||
const overlay = document.getElementById('batch-color-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
}
|
||||
|
||||
// 颜色池 - 解析单个颜色值(支持十进制和 #hex)
|
||||
function parseColorValue(raw) {
|
||||
const s = raw.trim();
|
||||
if (!s) return NaN;
|
||||
if (s.startsWith('#')) {
|
||||
const hex = s.slice(1);
|
||||
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
|
||||
const full = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
return parseInt(full, 16);
|
||||
}
|
||||
if (/^[0-9a-fA-F]{6}$/.test(hex)) {
|
||||
return parseInt(hex, 16);
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
const n = parseInt(s, 10);
|
||||
if (isNaN(n) || n < 0 || n > 16777215) return NaN;
|
||||
return n;
|
||||
}
|
||||
|
||||
// 颜色池 - 批量预览
|
||||
function updateBatchColorPreview() {
|
||||
const input = document.getElementById('batch-color-input');
|
||||
const preview = document.getElementById('batch-color-preview');
|
||||
if (!input || !preview) return;
|
||||
const parts = input.value.split(',');
|
||||
const html = parts.map(raw => {
|
||||
const c = parseColorValue(raw);
|
||||
if (isNaN(c)) return '';
|
||||
return \`<span class="batch-color-preview-swatch" style="background: #\${c.toString(16).padStart(6, '0')};"></span>\`;
|
||||
}).filter(Boolean).join('');
|
||||
preview.innerHTML = html || '<span class="color-pool-empty">输入颜色后预览</span>';
|
||||
}
|
||||
|
||||
// 颜色池 - 确认批量添加
|
||||
function confirmBatchColor() {
|
||||
const input = document.getElementById('batch-color-input');
|
||||
if (!input) return;
|
||||
const parts = input.value.split(',');
|
||||
const valid = parts.map(raw => parseColorValue(raw)).filter(c => !isNaN(c));
|
||||
if (valid.length === 0) {
|
||||
customAlert('未识别到有效颜色值');
|
||||
return;
|
||||
}
|
||||
const textarea = document.getElementById('text-value');
|
||||
const current = textarea.value.trim();
|
||||
const newVal = valid.map(String).join(',');
|
||||
textarea.value = current ? current + ',' + newVal : newVal;
|
||||
syncColorPoolDisplay();
|
||||
closeBatchColorDialog();
|
||||
}
|
||||
|
||||
// DANMU_OFFSET 快速配置 - 切换规则面板
|
||||
function toggleOffsetRulePanel() {
|
||||
const panel = document.getElementById('offset-rule-panel');
|
||||
@@ -712,9 +1002,18 @@ function appendOffsetRule() {
|
||||
const episode = document.getElementById('offset-episode').value.trim();
|
||||
const seconds = document.getElementById('offset-seconds').value.trim();
|
||||
|
||||
if (!anime) { customAlert('请输入剧名'); return; }
|
||||
if (!seconds) { customAlert('请输入偏移秒数'); return; }
|
||||
if (episode && !season) { customAlert('指定集时需要同时指定季'); return; }
|
||||
if (!anime) {
|
||||
customAlert('请输入剧名');
|
||||
return;
|
||||
}
|
||||
if (!seconds) {
|
||||
customAlert('请输入偏移秒数');
|
||||
return;
|
||||
}
|
||||
if (episode && !season) {
|
||||
customAlert('指定集时需要同时指定季');
|
||||
return;
|
||||
}
|
||||
|
||||
let path = anime;
|
||||
if (season) {
|
||||
@@ -738,7 +1037,6 @@ function appendOffsetRule() {
|
||||
const current = textarea.value.trim();
|
||||
textarea.value = current ? current + ',' + rule : rule;
|
||||
|
||||
// 重置表单
|
||||
document.getElementById('offset-anime').value = '';
|
||||
document.getElementById('offset-season').value = '';
|
||||
document.getElementById('offset-episode').value = '';
|
||||
|
||||
@@ -345,8 +345,7 @@ export function convertToDanmakuJson(contents, platform) {
|
||||
modified = true;
|
||||
}
|
||||
// 2.2 将白色弹幕转换为随机颜色,白、红、橙、黄、绿、青、蓝、紫、粉(模拟真实情况,增加白色出现概率)
|
||||
let colors = [16777215, 16777215, 16777215, 16777215, 16777215, 16777215, 16777215, 16777215,
|
||||
16744319, 16752762, 16774799, 9498256, 8388564, 8900346, 14204888, 16758465];
|
||||
let colors = globals.colorPool.split(',').map(c => parseInt(c.trim(), 10)).filter(c => !isNaN(c) && c >= 0 && c <= 16777215);
|
||||
let randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
if (globals.convertColor === 'color' && color === 16777215 && color !== randomColor) {
|
||||
colorCount++;
|
||||
|
||||
+22
-10
@@ -390,6 +390,17 @@ WidgetMetadata = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "colorPool",
|
||||
title: "自定义颜色池(CONVERT_COLOR为color时生效),不配置使用默认颜色池,格式:十进制颜色值逗号分隔",
|
||||
type: "input",
|
||||
placeholders: [
|
||||
{
|
||||
title: "默认颜色池",
|
||||
value: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "likeSwitch",
|
||||
title: "点赞功能开关,默认true",
|
||||
@@ -491,7 +502,7 @@ if (typeof window !== 'undefined') {
|
||||
let globals;
|
||||
async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch) {
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch) {
|
||||
// 将传入的参数设置到环境变量中,以便Globals可以访问它们
|
||||
const env = {};
|
||||
|
||||
@@ -516,6 +527,7 @@ async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServ
|
||||
if (danmuOffset !== undefined) env.DANMU_OFFSET = danmuOffset;
|
||||
if (convertTopBottomToScroll !== undefined) env.CONVERT_TOP_BOTTOM_TO_SCROLL = convertTopBottomToScroll;
|
||||
if (convertColor !== undefined) env.CONVERT_COLOR = convertColor;
|
||||
if (colorPool !== undefined) env.COLOR_POOL = colorPool;
|
||||
if (likeSwitch !== undefined) env.LIKE_SWITCH = likeSwitch;
|
||||
if (proxyUrl !== undefined) env.PROXY_URL = proxyUrl;
|
||||
if (tmdbApiKey !== undefined) env.TMDB_API_KEY = tmdbApiKey;
|
||||
@@ -585,11 +597,11 @@ const PREFIX_URL = "http://localhost:9321"
|
||||
async function searchDanmu(params) {
|
||||
const { tmdbId, type, title, season, link, videoUrl, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
|
||||
await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
|
||||
let simplifiedTitle = title
|
||||
// 如果启用了搜索关键字繁转简,则进行转换
|
||||
@@ -665,11 +677,11 @@ async function searchDanmu(params) {
|
||||
async function getDetailById(params) {
|
||||
const { animeId, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
|
||||
await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
|
||||
const response = await getBangumi(`${PREFIX_URL}/api/v2/bangumi/${animeId}`);
|
||||
const resJson = await response.json();
|
||||
@@ -684,11 +696,11 @@ async function getDetailById(params) {
|
||||
async function getCommentsById(params) {
|
||||
const { commentId, link, videoUrl, season, episode, tmdbId, type, title, segmentTime, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
|
||||
await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
|
||||
if (commentId) {
|
||||
const storeKey = season && episode ? `${tmdbId}.${season}.${episode}` : `${tmdbId}`;
|
||||
@@ -705,7 +717,7 @@ async function getCommentsById(params) {
|
||||
if (lastCommentId === commentId && segmentList) {
|
||||
return await getDanmuWithSegmentTime({ segmentTime, tmdbId, season, episode, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch })
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch })
|
||||
} else {
|
||||
Widget.storage.remove(storeKey);
|
||||
Widget.storage.remove(commentIdKey);
|
||||
@@ -731,11 +743,11 @@ async function getCommentsById(params) {
|
||||
async function getDanmuWithSegmentTime(params) {
|
||||
const { segmentTime, tmdbId, season, episode, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch } = params;
|
||||
|
||||
await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie,
|
||||
platformOrder, episodeTitleFilter, enableAnimeEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleFilter, animeTitleSimplified, blockedWords, groupMinute,
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
danmuLimit, danmuSimplifiedTraditional, danmuOffset, convertTopBottomToScroll, convertColor, colorPool, proxyUrl, tmdbApiKey, likeSwitch);
|
||||
|
||||
const storeKey = season && episode ? `${tmdbId}.${season}.${episode}` : `${tmdbId}`;
|
||||
const segmentList = Widget.storage.get(storeKey);
|
||||
|
||||
Reference in new issue
Block a user