koishi-plugin-starfx-bot 0.26.4 → 0.26.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,28 +63,20 @@
63
63
 
64
64
  /* 离场动画:只管滑出和透明度 */
65
65
  .slide-out-item {
66
- /* 重点:这里只管透明度和动画,不要在 keyframes 里写 margin */
67
66
  animation: slideOutToRight 0.4s cubic-bezier(0.2, 0, 0, 1) forwards;
68
-
69
- /* 关键:用 transition 平滑缩小高度,而不是用 animation */
70
- transition:
71
- max-height 0.4s ease,
72
- margin 0.4s ease,
73
- padding 0.4s ease,
74
- opacity 0.4s ease;
75
- max-height: 100px;
67
+ transition: all 0.4s cubic-bezier(0.2, 0, 0, 1);
76
68
  overflow: hidden;
77
- pointer-events: none;
78
69
  }
79
70
 
80
- /* 当进入删除状态时,触发 transition 坍塌 */
71
+ /* 当进入删除状态时,高度坍塌 */
81
72
  .slide-out-item[is-deleting="true"] {
82
- /*max-height: 0 !important;*/
73
+ max-height: 0 !important;
83
74
  margin-bottom: 0 !important;
84
75
  margin-top: 0 !important;
85
- /*padding-top: 0 !important;*/
86
- /*padding-bottom: 0 !important;*/
76
+ padding-top: 0 !important;
77
+ padding-bottom: 0 !important;
87
78
  opacity: 0;
79
+ border-width: 0;
88
80
  }
89
81
 
90
82
  .song-card:hover .font-bold {
@@ -126,25 +118,33 @@
126
118
  </head>
127
119
  <body class="bg-slate-50 min-h-screen p-4 sm:p-8 text-slate-900">
128
120
  <div id="app" class="max-w-md mx-auto" v-cloak>
129
- <header class="mb-6 flex justify-between items-end">
121
+ <header class="mb-6 flex justify-between items-start">
130
122
  <div>
131
123
  <h1 class="text-3xl font-black text-indigo-600">KTV Queue</h1>
132
124
  <p class="text-slate-400">房间: <%= roomId %></p>
133
125
  </div>
134
- <div class="text-[10px] text-slate-300 font-mono bg-slate-100 px-2 py-1 rounded">
135
- HASH: {{ lastHash.slice(0,6) }}
126
+ <div class="flex flex-col items-end gap-2">
127
+ <button @click="showSettings = true" class="p-2 bg-white rounded-xl shadow-sm border border-slate-200 text-slate-500 hover:text-indigo-600 transition">
128
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
129
+ <circle cx="12" cy="12" r="3"></circle>
130
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
131
+ </svg>
132
+ </button>
133
+ <div class="text-[10px] text-slate-300 font-mono bg-slate-100 px-2 py-1 rounded">
134
+ HASH: {{ lastHash.slice(0,6) }}
135
+ </div>
136
136
  </div>
137
137
  </header>
138
138
 
139
- <div class="bg-white p-5 rounded-2xl shadow-sm border border-slate-200 mb-6 space-y-3">
140
- <input v-model="form.title" @keyup.enter="add" class="w-full px-4 py-2 bg-slate-50 rounded-xl outline-none focus:ring-2 focus:ring-indigo-400 transition" placeholder="歌曲名称">
141
- <input v-model="form.url" @keyup.enter="add" class="w-full px-4 py-2 bg-slate-50 rounded-xl outline-none focus:ring-2 focus:ring-indigo-400 transition" placeholder="链接 (必填)">
142
- <button @click="add" class="w-full py-2 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 active:scale-95 transition shadow-lg shadow-indigo-100">
143
- 确认添加
139
+
140
+
141
+ <div class="mb-6 px-2">
142
+ <button @click="showAddModal = true" class="w-full py-4 bg-indigo-600 text-white font-bold rounded-2xl hover:bg-indigo-700 active:scale-95 transition shadow-lg shadow-indigo-200 flex items-center justify-center gap-2">
143
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
144
+ 添加新歌曲
144
145
  </button>
145
146
  </div>
146
147
 
147
-
148
148
  <draggable
149
149
  v-model="songs"
150
150
  item-key="id"
@@ -157,7 +157,7 @@
157
157
  >
158
158
  <template #item="{ element }">
159
159
  <div :key="element.id"
160
- @click="goToLink(element.url)"
160
+ @click="goToLink(element)"
161
161
  :is-deleting="element.isDeleting ? 'true' : 'false'"
162
162
  :class="['song-card bg-white mb-3 p-4 rounded-2xl shadow-sm border border-slate-200 flex items-center group',
163
163
  element.isNew ? 'slide-in-item' : '',
@@ -200,6 +200,34 @@
200
200
  </template>
201
201
  </draggable>
202
202
 
203
+ <transition name="modal">
204
+ <div v-if="showAddModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm" @click.self="showAddModal = false">
205
+ <div class="bg-white w-full max-w-sm rounded-[2.5rem] shadow-2xl p-6 space-y-4 animate-pop">
206
+ <h3 class="text-xl font-black text-slate-800 px-2">添加新歌曲</h3>
207
+
208
+ <div class="space-y-1">
209
+ <label class="text-[10px] font-bold text-indigo-400 ml-1 uppercase tracking-widest">智能提取 (粘贴B站分享文案)</label>
210
+ <textarea v-model="autoInput" @input="handleAutoRecognize" class="w-full px-4 py-3 bg-indigo-50/50 rounded-2xl outline-none border-2 border-transparent focus:border-indigo-200 transition text-sm h-24 resize-none" placeholder="在这里粘贴..."></textarea>
211
+ </div>
212
+
213
+ <div class="relative flex items-center justify-center py-2">
214
+ <div class="w-full border-t border-slate-100"></div>
215
+ <span class="absolute bg-white px-3 text-[10px] font-bold text-slate-300">手动核对</span>
216
+ </div>
217
+
218
+ <div class="space-y-3">
219
+ <input v-model="form.title" class="w-full px-4 py-3 bg-slate-50 rounded-xl outline-none focus:ring-2 focus:ring-indigo-400 text-sm" placeholder="歌曲标题">
220
+ <input v-model="form.url" class="w-full px-4 py-3 bg-slate-50 rounded-xl outline-none focus:ring-2 focus:ring-indigo-400 text-sm" placeholder="跳转链接">
221
+ </div>
222
+
223
+ <div class="flex gap-3 pt-2">
224
+ <button @click="showAddModal = false" class="flex-1 py-3 bg-slate-100 text-slate-500 font-bold rounded-xl transition">取消</button>
225
+ <button @click="handleAdd" class="flex-1 py-3 bg-indigo-600 text-white font-bold rounded-xl shadow-lg shadow-indigo-200 transition">确认添加</button>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </transition>
230
+
203
231
  <transition name="modal-fade">
204
232
  <div v-if="editingSong" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm" @click.self="editingSong = null">
205
233
  <div class="modal-container bg-white w-full max-w-sm rounded-3xl shadow-2xl border border-slate-100 p-6 space-y-4">
@@ -252,6 +280,71 @@
252
280
  </div>
253
281
  </transition>
254
282
 
283
+ <transition name="modal-fade">
284
+ <div v-if="pendingJumpUrl" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm" @click.self="pendingJumpUrl = null">
285
+ <div class="modal-container bg-white w-full max-w-sm rounded-3xl shadow-2xl border border-slate-100 p-6 space-y-6">
286
+ <div class="text-center">
287
+ <div class="w-16 h-16 bg-indigo-50 text-indigo-500 rounded-full flex items-center justify-center mx-auto mb-4">
288
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
289
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
290
+ <polyline points="15 3 21 3 21 9"></polyline>
291
+ <line x1="10" y1="14" x2="21" y2="3"></line>
292
+ </svg>
293
+ </div>
294
+ <h3 class="text-xl font-bold text-slate-800">即将离开页面</h3>
295
+ <p class="text-slate-500 mt-2 text-sm">确认要前往播放 <span class="font-semibold text-indigo-600">"{{ jumpSongTitle }}"</span> 吗?</p>
296
+ </div>
297
+ <div class="flex space-x-3">
298
+ <button @click="pendingJumpUrl = null" class="flex-1 py-3 bg-slate-100 text-slate-600 font-bold rounded-xl hover:bg-slate-200 transition">
299
+ 留在本页
300
+ </button>
301
+ <button @click="confirmJump" class="flex-1 py-3 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 shadow-lg shadow-indigo-200 transition">
302
+ 立即前往
303
+ </button>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </transition>
308
+
309
+ <transition name="modal-fade">
310
+ <div v-if="showSettings" class="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm" @click.self="showSettings = false">
311
+ <div class="modal-container bg-white w-full max-w-sm rounded-3xl shadow-2xl border border-slate-100 p-6 space-y-6">
312
+ <h3 class="text-xl font-bold text-slate-800">偏好设置</h3>
313
+
314
+ <div class="space-y-4">
315
+ <div class="p-4 bg-slate-50 rounded-2xl border border-slate-100">
316
+ <div class="flex items-center justify-between mb-2">
317
+ <span class="font-bold text-slate-700">跳转方式</span>
318
+ <div class="relative flex bg-slate-200 p-1 rounded-xl w-32 h-9 overflow-hidden">
319
+ <div class="absolute top-1 left-1 bottom-1 w-[calc(50%-4px)] bg-white rounded-lg shadow-sm transition-transform duration-300 ease-[cubic-bezier(0.4,1.2,0.3,1)]"
320
+ :style="{ transform: jumpMode === 'app' ? 'translateX(100%)' : 'translateX(0)' }"></div>
321
+
322
+ <button @click="jumpMode = 'web'" class="relative z-10 flex-1 text-xs font-bold transition-colors duration-200" :class="jumpMode === 'web' ? 'text-indigo-600' : 'text-slate-500'">网页</button>
323
+ <button @click="jumpMode = 'app'" class="relative z-10 flex-1 text-xs font-bold transition-colors duration-200" :class="jumpMode === 'app' ? 'text-indigo-600' : 'text-slate-500'">App</button>
324
+ </div>
325
+ </div>
326
+ <p class="text-[10px] text-slate-400">网页模式打开 H5 页面,App 尝试唤起客户端</p>
327
+ </div>
328
+
329
+ <div class="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100">
330
+ <div>
331
+ <div class="font-bold text-slate-700">直接跳转</div>
332
+ <div class="text-[10px] text-slate-400">点击歌曲后不再弹出确认框</div>
333
+ </div>
334
+ <button @click="autoJump = !autoJump"
335
+ :class="['w-12 h-6 rounded-full transition-colors duration-300 relative focus:outline-none flex items-center', autoJump ? 'bg-indigo-600 shadow-inner' : 'bg-slate-300']">
336
+ <div :class="['absolute bg-white w-4 h-4 rounded-full shadow transition-all duration-300 ease-[cubic-bezier(0.34,1.2,0.5,1)]', autoJump ? 'left-7 scale-110' : 'left-1 scale-100']"></div>
337
+ </button>
338
+ </div>
339
+ </div>
340
+
341
+ <button @click="showSettings = false" class="w-full py-3 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 active:scale-95 transition shadow-lg shadow-indigo-100">
342
+ 完成
343
+ </button>
344
+ </div>
345
+ </div>
346
+ </transition>
347
+
255
348
 
256
349
  <div v-if="songs.length === 0" class="text-center py-20 text-slate-300">
257
350
  列表是空的
@@ -264,51 +357,77 @@
264
357
  components: { draggable: window.vuedraggable },
265
358
  setup() {
266
359
  const roomId = "<%= roomId %>"
267
- const songs = ref([])
268
360
  const EMPTY_HASH = "EMPTY_LIST_HASH"; // 与后端 getHash 函数中的占位符一致
269
- const lastHash = ref(EMPTY_HASH)
270
- const isDragging = ref(false)
361
+ const lastHash = ref(EMPTY_HASH);
362
+ const isDragging = ref(false);
363
+ const deletingSong = ref(null);
364
+ const editingSong = ref(null);
365
+ const editForm = ref({ title: '', url: '' });
366
+ const songs = ref([]);
271
367
  const form = ref({ title: '', url: '' })
272
- const pathSegments = window.location.pathname.split('/').filter(Boolean);
273
- // 找到 roomId 在路径中的索引位置
274
- const roomIdx = pathSegments.indexOf(roomId);
275
-
276
- let apiRoot;
277
- if (roomIdx !== -1) {
278
- // roomId 之前的所有部分,重新拼起来
279
- // 如果是 ["songRoom", "1145"] -> prefix 就是 "/songRoom"
280
- // 如果是 ["1145"] -> prefix 就是 ""
281
- const prefix = pathSegments.slice(0, roomIdx).join('/');
282
- const slash = prefix ? '/' : '';
283
- apiRoot = `${slash}${prefix}/api/${roomId}`;
284
- } else {
285
- // 兜底逻辑:如果路径里竟然没找到 roomId,回退到最简单的相对路径
286
- apiRoot = `api/${roomId}`;
368
+ const pendingJumpUrl = ref(null); // 存储待跳转的 URL
369
+ const jumpSongTitle = ref(''); // 存储待跳转的歌曲标题
370
+ const showSettings = ref(false);
371
+ const showAddModal = ref(false);
372
+ const autoInput = ref('');
373
+ const jumpMode = ref(localStorage.getItem('ktv_jump_mode') || 'web');
374
+ const autoJump = ref(localStorage.getItem('ktv_auto_jump') === 'true');
375
+ const commitApiUrl = "api/songOperation"
376
+ const loadSongListUrl = "api/songListInfo"
377
+
378
+ Vue.watch(jumpMode, (val) => localStorage.setItem('ktv_jump_mode', val));
379
+ Vue.watch(autoJump, (val) => localStorage.setItem('ktv_auto_jump', val));
380
+
381
+ if (!window.isSecureContext) {
382
+ console.error("当前环境不是安全上下文,Web Crypto API 将不可用!");
383
+ alert("当前环境不是安全上下文,Web Crypto API 将不可用,这可能会导致一些同步的问题");
287
384
  }
288
385
 
289
386
 
290
- // 1. 修改 commitOp,防止前端标记传给后端导致 500 错误
387
+ async function getHash(songs) {
388
+ if (!window.isSecureContext) return lastHash.value;
389
+ if (!songs || songs.length === 0) return "EMPTY_LIST_HASH";
390
+
391
+ // 将歌曲对象数组转为固定格式的字符串
392
+ const str = songs.map(s => `${s.id}:${s.title}:${s.url}`).join('|');
393
+ const msgBuffer = new TextEncoder().encode(str);
394
+
395
+ // 调用原生 Web Crypto API 计算哈希
396
+ const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgBuffer);
397
+
398
+ // 将 ArrayBuffer 转为十六进制字符串
399
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
400
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
401
+ }
402
+
291
403
  const commitOp = async (opData) => {
292
- const cleanSong = opData.song ? { ...opData.song } : null;
293
- if (cleanSong) {
294
- // 剔除所有前端动画状态标记,防止后端校验失败或存储多余字段
295
- const keysToRemove = ['isNew', 'isDeleting', 'isMoved', 'isAffected'];
296
- keysToRemove.forEach(k => delete cleanSong[k]);
404
+ let cleanSong = null;
405
+ if (opData.song) {
406
+ const { id, title, url } = opData.song;
407
+ cleanSong = { id, title, url };
297
408
  }
298
409
 
299
410
  try {
300
- const res = await fetch(apiRoot, {
411
+ const res = await fetch(`${commitApiUrl}?roomId=${roomId}`, {
301
412
  method: 'POST',
302
413
  headers: { 'Content-Type': 'application/json' },
303
414
  body: JSON.stringify({
304
415
  idArrayHash: lastHash.value, // 这个 Hash 现在代表了旧列表的内容+顺序
305
- ...opData,
416
+ toIndex: opData.toIndex,
306
417
  song: cleanSong
307
418
  })
308
419
  }).then(r => r.json());
309
420
 
310
421
  if (res.success) {
422
+ if (await getHash(songs.value) !== res.hash) await load();
311
423
  lastHash.value = res.hash;
424
+ if (res.song && opData.song) {
425
+ const localSong = songs.value.find(s => s.id === opData.song.id);
426
+ if (localSong) {
427
+ localSong.url = res.song.url; // 此时 url 已经是 bilibili://...
428
+ localSong.id = res.song.id; // 此时 id 已经是 BV...
429
+ }
430
+ }
312
431
  return true;
313
432
  } else if (res.code === 'REJECT') {
314
433
  // 如果被拒绝,说明前端 Hash 过时或列表已空但前端不知道
@@ -321,14 +440,107 @@
321
440
  return false;
322
441
  }
323
442
 
324
- // 2. 修改 add 函数,计算正确的 toIndex
443
+ const handleAdd = async () => {
444
+ if(!form.value.title || !form.value.url) return;
445
+ await add();
446
+ showAddModal.value = false;
447
+ autoInput.value = ''; // 清空识别框
448
+ };
449
+
450
+ const handleAutoRecognize = () => {
451
+ let raw = autoInput.value.trim();
452
+ if (!raw) return;
453
+
454
+ // 提取链接
455
+ const urlMatch = raw.match(/https?:\/\/(?:[a-zA-Z0-9-]+\.)?(?:bilibili\.com|b23\.tv)\/[a-zA-Z0-9/._?=-]+/i);
456
+ if (urlMatch) form.value.url = urlMatch[0];
457
+
458
+ // 初始清理:只去掉链接,保留所有文字和括号
459
+ let title = raw.replace(/https?:\/\/\S+/g, '').trim();
460
+
461
+ // "-哔哩哔哩" 的最外层括号
462
+ // 【内容-哔哩哔哩】 -> 内容
463
+ title = title.replace(/[【](.*)-哔哩哔哩[】]/i, '$1');
464
+ // 如果没被括号包住,也直接删掉后缀
465
+ title = title.replace(/-哔哩哔哩/i, '');
466
+
467
+ const blacklist = /(ニコカラ|on[ /]?vocal|off[ /]?vocal|on\/off vocal|假名|字幕|罗马音|和声伴奏|纯k投屏|自用|完整版MV|KTV字幕|KTV|Karaoke|搬运|カラオケ|nicokara|卡拉OK|歌词|分唱)/gi;
468
+
469
+ function cleanTitleNested(title) {
470
+
471
+ const brackets = { '】': '【', ']': '[', ')': '(', ')': '(', '』': '『', '」': '「' };
472
+ const leftBrackets = Object.values(brackets);
473
+
474
+ // 使用数组操作,方便根据索引标记删除
475
+ let chars = title.split('');
476
+
477
+ // 辅助函数:检查一段区间是否已经被删除(全部为空字符串)
478
+ const isAlreadyDeleted = (start, end) => {
479
+ return chars.slice(start, end + 1).every(c => c === "");
480
+ };
481
+
482
+ let stack = [];
483
+ for (let i = 0; i < chars.length; i++) {
484
+ let char = chars[i];
485
+
486
+ if (leftBrackets.includes(char)) {
487
+ stack.push({ type: char, index: i });
488
+ } else if (brackets[char]) {
489
+ // 查找栈中最近的匹配左括号
490
+ let lastMatchIdx = -1;
491
+ for (let j = stack.length - 1; j >= 0; j--) {
492
+ if (stack[j].type === brackets[char]) {
493
+ lastMatchIdx = j;
494
+ break;
495
+ }
496
+ }
497
+
498
+ if (lastMatchIdx !== -1) {
499
+ let left = stack.splice(lastMatchIdx, 1)[0];
500
+ let start = left.index;
501
+ let end = i;
502
+
503
+ // 提取当前层级的内容进行检测
504
+ // 注意:这里需要拿 chars 里的实时内容,如果子层被删了,子层位置会是空字符
505
+ let currentContent = chars.slice(start, end + 1).join('');
506
+
507
+ blacklist.lastIndex = 0;
508
+ if (blacklist.test(currentContent)) {
509
+ // 命中黑名单,将 chars 数组对应区间全部置为空字符串
510
+ for (let k = start; k <= end; k++) {
511
+ chars[k] = "";
512
+ }
513
+ }
514
+ }
515
+ }
516
+ }
517
+
518
+ // 合并结果,并清理多余空格
519
+ let result = chars.join('');
520
+
521
+ // 清理“空括号”:匹配任何类型的左括号 + 0或多个空格 + 匹配的右括号
522
+ // 这里的正则涵盖了:(), [], {}, 【】, (), 『』, 「」
523
+ const emptyBrackets = /(\(\s*\)|\[\s*\]|【\s*】|(\s*)|『\s*』|「\s*」)/g;
524
+
525
+ // 循环清理,防止出现 [【】] 这种嵌套空括号清理不干净的情况
526
+ while (emptyBrackets.test(result)) {
527
+ result = result.replace(emptyBrackets, '');
528
+ }
529
+
530
+ // 最后清理多余空格并返回
531
+ return result.replace(/\s+/g, ' ').trim();
532
+ }
533
+
534
+ title = cleanTitleNested(title);
535
+
536
+ // 6. 清理残留在括号外的游离标签
537
+ title = title.replace(blacklist, "");
538
+ form.value.title = title;
539
+ };
540
+
325
541
  const add = async () => {
326
542
  let rawUrl = form.value.url.trim();
327
543
  if(!form.value.title || !rawUrl) return;
328
- // 自动补充协议头
329
- if (!/^https?:\/\//i.test(rawUrl)) {
330
- rawUrl = 'https://' + rawUrl;
331
- }
332
544
 
333
545
  // 计算有效长度(排除正在删除的)
334
546
  const effectiveLen = songs.value.filter(s => !s.isDeleting).length;
@@ -355,10 +567,6 @@
355
567
  if (!success) await load();
356
568
  }
357
569
 
358
- // 3. 修改 remove 函数,实现先离场再删除
359
- // 在 setup 内定义新变量
360
- const deletingSong = ref(null);
361
-
362
570
  // 第一步:点击垃圾桶图标,仅记录要删除的对象并显示弹窗
363
571
  const remove = (songObj) => {
364
572
  deletingSong.value = songObj;
@@ -421,16 +629,15 @@
421
629
  return result;
422
630
  }
423
631
 
424
- // load 函数修改
425
632
  const load = async () => {
426
633
  if (isDragging.value) return;
427
634
  try {
428
- const url = `${apiRoot}?lastHash=${lastHash.value}`;
635
+ const url = `${loadSongListUrl}?roomId=${roomId}&lastHash=${(await getHash(songs.value))}`;
429
636
  const res = await fetch(url).then(r => r.json());
430
637
 
431
638
  if (res.changed) {
432
639
  const oldSongs = [...songs.value];
433
- const newSongsData = res.list || []; // 防御性处理空返回
640
+ const newSongsData = res.list || []; // 处理空返回
434
641
 
435
642
  // 如果新数据就是空的,直接赋值并更新 Hash,跳过后续复杂的 LIS 计算
436
643
  if (newSongsData.length === 0) {
@@ -439,11 +646,11 @@
439
646
  return;
440
647
  }
441
648
 
442
- // 1. 计算 ID 映射
649
+ // 计算 ID 映射
443
650
  const oldIdMap = new Map();
444
651
  oldSongs.forEach((s, i) => { if (!s.isDeleting) oldIdMap.set(s.id, i); });
445
652
 
446
- // 2. 识别“主动移动”的 ID
653
+ // 识别“主动移动”的 ID
447
654
  const source = newSongsData.map(s => oldIdMap.has(s.id) ? oldIdMap.get(s.id) : -1);
448
655
  const lisIndices = new Set(getLISIndices(source));
449
656
 
@@ -456,7 +663,7 @@
456
663
  }
457
664
  });
458
665
 
459
- // 3. 第一阶段:让删除项和“改动项”一起执行退出动画
666
+ // 让删除项和“改动项”一起执行退出动画
460
667
  const newIdSet = new Set(newSongsData.map(s => s.id));
461
668
  oldSongs.forEach(s => {
462
669
  // 如果是服务器删了,或者它是主动移动项,执行退出
@@ -465,7 +672,7 @@
465
672
  }
466
673
  });
467
674
 
468
- // 4. 第二阶段:等待退出动画完成
675
+ // 等待退出动画完成
469
676
  setTimeout(() => {
470
677
  // 构建最终列表
471
678
  songs.value = newSongsData.map((s, newIdx) => {
@@ -478,55 +685,83 @@
478
685
 
479
686
  return {
480
687
  ...s,
481
- // 如果是真新歌,或者是我们刚才主动剔除的改动项,施加入场动画
688
+ // 入场动画
482
689
  isMoved: isActiveMove,
483
- isNew: (isNew || isActiveMove) && oldSongs.length > 0,
690
+ isNew: (isNew || isActiveMove), //&& oldSongs.length > 0,
484
691
  isAffected: isAffected
485
692
  };
486
693
  });
487
694
 
488
695
  lastHash.value = res.hash;
489
696
 
490
- // 5. 第三阶段:清理状态
697
+ // 清理状态
491
698
  setTimeout(() => {
492
699
  songs.value.forEach(s => {
493
700
  s.isNew = s.isAffected = false;
494
701
  });
495
702
  }, 600);
496
- }, 350); // 这里的 400ms 对应你 CSS 里 slide-out 的时间
703
+ }, 350);
497
704
  }
498
705
  } catch (e) { console.error("Load Error:", e); }
499
706
  };
500
707
 
501
- // setup() 内部定义
502
- const goToLink = (url) => {
503
- if (url) {
504
- // 确保 URL 带有协议头
505
- const link = /^https?:\/\//i.test(url) ? url : `https://${url}`;
506
- window.open(link, '_blank');
708
+ const goToLink = (song) => {
709
+ if (song && song.url) {
710
+ pendingJumpUrl.value = /^https?:\/\//i.test(song.url) ? song.url : `https://${song.url}`;
711
+ jumpSongTitle.value = song.title;
712
+
713
+ if (autoJump.value) {
714
+ confirmJump(); // 直接执行跳转
715
+ }
716
+ }
717
+ };
718
+
719
+ // 修改后的跳转函数
720
+ const confirmJump = () => {
721
+ if (pendingJumpUrl.value) {
722
+ const url = pendingJumpUrl.value;
723
+ // 匹配固定的 10 位 BV 号
724
+ const bvMatch = url.match(/BV[a-zA-Z0-9]{10}/i);
725
+ const bvId = bvMatch ? bvMatch[0] : null;
726
+
727
+ if (jumpMode.value === 'app') {
728
+ // --- 目标是 App ---
729
+ if (bvId) {
730
+ // 转换格式: bilibili://video/BV1234567890
731
+ window.location.href = `bilibili://video/${bvId}`;
732
+ } else {
733
+ window.location.href = url;
734
+ }
735
+ } else {
736
+ // --- 目标是 Web ---
737
+ if (bvId) {
738
+ // 转换格式: https://m.bilibili.com/video/BV1234567890
739
+ window.open(`https://m.bilibili.com/video/${bvId}`, '_blank');
740
+ } else {
741
+ window.open(url, '_blank');
742
+ }
743
+ }
744
+ pendingJumpUrl.value = null;
507
745
  }
508
746
  };
509
747
 
510
748
  const onDragChange = async (evt) => {
511
749
  isDragging.value = false;
512
750
  if (evt.moved) {
513
- const { element, newIndex, oldIndex } = evt.moved;
751
+ const { element, newIndex } = evt.moved;
514
752
  // 修正向后移动时的标尺偏移
515
- let targetK = newIndex;
516
- if (newIndex > oldIndex) {
753
+ // let targetK = newIndex;
754
+ /* if (newIndex > oldIndex) {
517
755
  targetK = newIndex + 1;
518
- }
756
+ }*/
519
757
 
520
758
  await commitOp({
521
759
  song: element,
522
- toIndex: targetK // 发送修正后的标尺
760
+ toIndex: newIndex // 发送修正后的标尺
523
761
  });
524
762
  }
525
763
  }
526
764
 
527
- const editingSong = ref(null); // 存储当前正在编辑的对象引用
528
- const editForm = ref({ title: '', url: '' });
529
-
530
765
  // 点击编辑按钮触发
531
766
  const startEdit = (song) => {
532
767
  editingSong.value = song;
@@ -535,14 +770,13 @@
535
770
  };
536
771
 
537
772
  const moveToTop = async (song) => {
538
- // 1. 如果已经在第一位,无需操作
773
+ // 如果已经在第一位,无需操作
539
774
  if (songs.value[0]?.id === song.id) return;
540
775
 
541
- // 2. 这里的逻辑与 load 中的“主动移动”一致
542
- // 我们先在本地模拟移动,触发 LIS 识别逻辑和动画
776
+ // 这里的逻辑与 load 中的“主动移动”一致
543
777
  const oldIndex = songs.value.findIndex(s => s.id === song.id);
544
778
  if (oldIndex !== -1) {
545
- // 标记为正在移动,以便 load 函数能正确处理动画(如果此时刚好触发 load)
779
+ // 标记为正在移动
546
780
  song.isDeleting = true;
547
781
 
548
782
  setTimeout(async () => {
@@ -556,19 +790,19 @@
556
790
  movedItem.isTop = true;
557
791
  movedItem.isNew = true;
558
792
 
559
- // 3. 发送给后端,toIndex: 0 代表第 0 个锚点(首位)
793
+ // 发送给后端,toIndex: 目标顺位
560
794
  const success = await commitOp({
561
795
  song: movedItem,
562
796
  toIndex: 0
563
797
  });
564
798
 
565
799
  if (!success) {
566
- await load(); // 失败则重载列表
800
+ await load();
567
801
  } else {
568
802
  // 成功后延迟移除高亮
569
803
  setTimeout(() => { movedItem.isTop = false; movedItem.isNew = false; }, 1000);
570
804
  }
571
- }, 300); // 这里的延迟为了配合 CSS 离场感
805
+ }, 300);
572
806
  }
573
807
  };
574
808
 
@@ -603,15 +837,24 @@
603
837
 
604
838
 
605
839
 
840
+
841
+
606
842
  let timer
607
843
  onMounted(() => {
608
844
  load()
609
845
  // 每 3 秒同步一次数据
610
- timer = setInterval(load, 3000)
846
+ timer = setInterval(load, 5000)
611
847
  })
612
848
  onUnmounted(() => clearInterval(timer))
613
849
 
614
- return { songs, form, add, remove, onDragChange, lastHash, isDragging, goToLink, startEdit, saveEdit, editingSong, editForm, moveToTop, deletingSong, confirmDelete }
850
+ return { songs, form, add, remove, onDragChange,
851
+ lastHash, isDragging, goToLink, startEdit,
852
+ saveEdit, editingSong, editForm, moveToTop,
853
+ deletingSong, confirmDelete, pendingJumpUrl,
854
+ jumpSongTitle, confirmJump, showSettings,
855
+ jumpMode, autoJump,showAddModal, autoInput,
856
+ handleAutoRecognize, handleAdd,
857
+ }
615
858
  }
616
859
  }).mount('#app')
617
860
  </script>
package/lib/index.d.ts CHANGED
@@ -3,16 +3,6 @@ export declare const name = "starfx-bot";
3
3
  export declare const inject: {
4
4
  optional: string[];
5
5
  };
6
- declare module "@koishijs/cache" {
7
- interface Tables {
8
- ktv_room: Song[];
9
- }
10
- }
11
- interface Song {
12
- id: string;
13
- title: string;
14
- url?: string;
15
- }
16
6
  export declare let baseDir: string;
17
7
  export declare let assetsDir: string;
18
8
  export declare const starfxLogger: Logger;
@@ -64,7 +54,6 @@ export interface Config {
64
54
  originImg: boolean;
65
55
  originImgRSSUrl: string;
66
56
  filePathToBase64: boolean;
67
- ktvServer: boolean;
68
57
  featureControl: Array<{
69
58
  functionName: string;
70
59
  whitelist: boolean;
package/lib/index.js CHANGED
@@ -54,9 +54,7 @@ module.exports = __toCommonJS(src_exports);
54
54
  var fs2 = __toESM(require("node:fs"));
55
55
  var import_node_path4 = __toESM(require("node:path"));
56
56
  var import_koishi5 = require("koishi");
57
- var crypto = __toESM(require("crypto"));
58
57
  var import_mime_types = __toESM(require("mime-types"));
59
- var import_ejs = __toESM(require("ejs"));
60
58
 
61
59
  // package.json
62
60
  var package_default = {
@@ -65,7 +63,7 @@ var package_default = {
65
63
  contributors: [
66
64
  "StarFreedomX <starfreedomx@outlook.com>"
67
65
  ],
68
- version: "0.26.3",
66
+ version: "0.26.5",
69
67
  main: "lib/index.js",
70
68
  typings: "lib/index.d.ts",
71
69
  files: [
@@ -109,8 +107,7 @@ var package_default = {
109
107
  "http-proxy-agent": "^7.0.2",
110
108
  "https-proxy-agent": "^7.0.6",
111
109
  "mime-types": "^3.0.1",
112
- "rss-parser": "^3.13.0",
113
- ejs: "^3.1.10"
110
+ "rss-parser": "^3.13.0"
114
111
  },
115
112
  devDependencies: {
116
113
  "@biomejs/biome": "2.3.7",
@@ -741,7 +738,6 @@ function handleRoll(session) {
741
738
  parts.push(element);
742
739
  }
743
740
  }
744
- console.log(parts);
745
741
  parts.shift();
746
742
  if (!parts) return session.text(".noParam");
747
743
  const last = session.elements[session.elements.length - 1];
@@ -773,7 +769,7 @@ function handleRoll(session) {
773
769
  }
774
770
  __name(handleRoll, "handleRoll");
775
771
  function getPoints(session, num, noodles) {
776
- if (!Number.isInteger(num) || !Number.isInteger(noodles) || num < 0 || noodles > 0)
772
+ if (!Number.isInteger(num) || !Number.isInteger(noodles) || num <= 0 || noodles <= 0)
777
773
  return session.text(".invalid");
778
774
  if (num > 20 || noodles > 1e8) return session.text(".too-many");
779
775
  const points = Array(num).fill(0).map(() => Math.floor(Math.random() * noodles + 1));
@@ -1219,7 +1215,7 @@ __name(getXImageBase64, "getXImageBase64");
1219
1215
  // src/index.ts
1220
1216
  var name = "starfx-bot";
1221
1217
  var inject = {
1222
- optional: ["skia", "QhzySharp", "server", "cache"]
1218
+ optional: ["skia", "QhzySharp"]
1223
1219
  };
1224
1220
  var baseDir;
1225
1221
  var assetsDir;
@@ -1301,8 +1297,7 @@ var Config = import_koishi5.Schema.intersect([
1301
1297
  filePathToBase64: import_koishi5.Schema.boolean().default(false).description(
1302
1298
  "在消息发送前检查是否有file://,如果有那么转换为base64再发送"
1303
1299
  ),
1304
- originImg: import_koishi5.Schema.boolean().default(false).description("根据链接获取原图开关"),
1305
- ktvServer: import_koishi5.Schema.boolean().default(false).description('开启ktv web服务器,访问地址是"<a href="/songRoom">koishi地址/songRoom</a>"')
1300
+ originImg: import_koishi5.Schema.boolean().default(false).description("根据链接获取原图开关")
1306
1301
  }).description("自用功能"),
1307
1302
  import_koishi5.Schema.union([
1308
1303
  import_koishi5.Schema.object({
@@ -1648,259 +1643,6 @@ function apply(ctx, cfg) {
1648
1643
  }
1649
1644
  });
1650
1645
  }
1651
- if (cfg.ktvServer && ctx.server) {
1652
- let getHash = function(songs) {
1653
- if (!songs || songs.length === 0) return "EMPTY_LIST_HASH";
1654
- const str = songs.map((s) => `${s.id}:${s.title}`).join("|");
1655
- return crypto.createHash("md5").update(str).digest("hex");
1656
- }, songOperation = function(nowSongs, songIdArray, ops) {
1657
- const latestSongMap = /* @__PURE__ */ new Map();
1658
- if (Array.isArray(nowSongs)) {
1659
- nowSongs.forEach((s) => s && s.id && latestSongMap.set(s.id, s));
1660
- }
1661
- [...ops].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).forEach((op) => {
1662
- if (op?.song?.id && op.toIndex !== -1) {
1663
- latestSongMap.set(op.song.id, op.song);
1664
- }
1665
- });
1666
- class ListNode {
1667
- static {
1668
- __name(this, "ListNode");
1669
- }
1670
- val;
1671
- prev = null;
1672
- next = null;
1673
- constructor(val) {
1674
- this.val = val;
1675
- }
1676
- }
1677
- const head = new ListNode("HEAD");
1678
- let current = head;
1679
- const idNodes = /* @__PURE__ */ new Map();
1680
- const anchorNodes = /* @__PURE__ */ new Map();
1681
- for (let i = 0; i <= (songIdArray?.length || 0); i++) {
1682
- const anchorNode = new ListNode(i);
1683
- anchorNodes.set(i, anchorNode);
1684
- current.next = anchorNode;
1685
- anchorNode.prev = current;
1686
- current = anchorNode;
1687
- if (i < songIdArray.length) {
1688
- const id = songIdArray[i];
1689
- if (id !== void 0 && id !== null) {
1690
- const idNode = new ListNode(id);
1691
- idNodes.set(id, idNode);
1692
- current.next = idNode;
1693
- idNode.prev = current;
1694
- current = idNode;
1695
- }
1696
- }
1697
- }
1698
- ops.forEach((op) => {
1699
- if (!op?.song?.id) return;
1700
- const { song, toIndex } = op;
1701
- let node = idNodes.get(song.id);
1702
- if (node && node.prev) {
1703
- const prevNode = node.prev;
1704
- const nextNode = node.next;
1705
- prevNode.next = nextNode;
1706
- if (nextNode) {
1707
- nextNode.prev = prevNode;
1708
- }
1709
- node.prev = null;
1710
- node.next = null;
1711
- }
1712
- if (toIndex === -1) {
1713
- idNodes.delete(song.id);
1714
- return;
1715
- }
1716
- if (!node) {
1717
- node = new ListNode(song.id);
1718
- idNodes.set(song.id, node);
1719
- }
1720
- const targetAnchor = anchorNodes.get(toIndex);
1721
- if (targetAnchor && targetAnchor.prev) {
1722
- const before = targetAnchor.prev;
1723
- before.next = node;
1724
- node.prev = before;
1725
- node.next = targetAnchor;
1726
- targetAnchor.prev = node;
1727
- }
1728
- });
1729
- const result = [];
1730
- let p = head.next;
1731
- while (p !== null) {
1732
- if (typeof p.val === "string" && p.val !== "HEAD") {
1733
- const songData = latestSongMap.get(p.val);
1734
- if (songData) {
1735
- result.push(songData);
1736
- }
1737
- }
1738
- p = p.next;
1739
- }
1740
- return result;
1741
- };
1742
- __name(getHash, "getHash");
1743
- __name(songOperation, "songOperation");
1744
- const templatePath = import_node_path4.default.resolve(assetsDir, "./songRoom.ejs");
1745
- const templateStr = fs2.readFileSync(templatePath, "utf-8");
1746
- const roomOpCache = {};
1747
- const roomSongsCache = {};
1748
- ctx.setInterval(() => {
1749
- const now = Date.now();
1750
- for (const roomId in roomOpCache) {
1751
- roomOpCache[roomId] = roomOpCache[roomId].filter((log) => now - log.timestamp < 5 * 60 * 1e3);
1752
- if (roomOpCache[roomId].length === 0) delete roomOpCache[roomId];
1753
- }
1754
- }, 5 * 60 * 1e3);
1755
- ctx.server.get("/songRoom/api/:roomId", async (koaCtx) => {
1756
- const { roomId } = koaCtx.params;
1757
- const { lastHash: clientHash } = koaCtx.query;
1758
- if (!roomSongsCache[roomId]) {
1759
- const dbData = await ctx.cache.get("ktv_room", roomId);
1760
- roomSongsCache[roomId] = dbData || [];
1761
- }
1762
- const currentSongs = roomSongsCache[roomId];
1763
- const serverHash = getHash(currentSongs);
1764
- if (!roomOpCache[roomId] || roomOpCache[roomId].length === 0) {
1765
- roomOpCache[roomId] = [{
1766
- idArray: currentSongs.map((s) => s.id),
1767
- hash: serverHash,
1768
- song: null,
1769
- toIndex: -1,
1770
- timestamp: Date.now()
1771
- }];
1772
- }
1773
- if (clientHash && clientHash === serverHash) {
1774
- return koaCtx.body = { changed: false, hash: serverHash };
1775
- }
1776
- koaCtx.body = {
1777
- changed: true,
1778
- list: currentSongs,
1779
- hash: serverHash
1780
- };
1781
- });
1782
- ctx.server.post("/songRoom/api/:roomId", async (koaCtx) => {
1783
- const { roomId } = koaCtx.params;
1784
- const body = koaCtx.request["body"];
1785
- const { idArrayHash, song, toIndex } = body;
1786
- if (!roomSongsCache[roomId]) {
1787
- roomSongsCache[roomId] = await ctx.cache.get("ktv_room", roomId) || [];
1788
- }
1789
- if (!roomOpCache[roomId]) {
1790
- roomOpCache[roomId] = [{
1791
- idArray: roomSongsCache[roomId].map((s) => s.id),
1792
- hash: getHash(roomSongsCache[roomId]),
1793
- song: null,
1794
- toIndex: -1,
1795
- timestamp: Date.now()
1796
- }];
1797
- }
1798
- const logs = roomOpCache[roomId];
1799
- const hitIdx = logs.findIndex((l) => l.hash === idArrayHash);
1800
- if (hitIdx === -1) {
1801
- return koaCtx.body = { success: false, code: "REJECT" };
1802
- }
1803
- const baseLog = logs[hitIdx];
1804
- const spotIds = [...baseLog.idArray];
1805
- const nowSongs = [...roomSongsCache[roomId]];
1806
- const currentOp = {
1807
- idArray: [],
1808
- hash: "",
1809
- song,
1810
- toIndex,
1811
- timestamp: Date.now()
1812
- };
1813
- const laterOps = [...logs.slice(hitIdx + 1), currentOp];
1814
- try {
1815
- const finalSongs = songOperation(nowSongs, spotIds, laterOps);
1816
- const finalIds = finalSongs.map((s) => s.id);
1817
- const finalHash = getHash(finalSongs);
1818
- currentOp.idArray = finalIds;
1819
- currentOp.hash = finalHash;
1820
- logs.push(currentOp);
1821
- if (logs.length > 50) logs.shift();
1822
- roomSongsCache[roomId] = finalSongs;
1823
- await ctx.cache.set(`ktv_room`, roomId, finalSongs);
1824
- koaCtx.body = { success: true, hash: finalHash };
1825
- } catch (e) {
1826
- console.error("Operation re-run failed:", e);
1827
- koaCtx.body = { success: false, code: "REJECT" };
1828
- }
1829
- });
1830
- ctx.server.get("/songRoom/:roomId", async (koaCtx) => {
1831
- const { roomId } = koaCtx.params;
1832
- const html = import_ejs.default.render(templateStr, {
1833
- roomId,
1834
- pageTitle: `KTV 房间 - ${roomId}`
1835
- });
1836
- koaCtx.type = "html";
1837
- koaCtx.body = html;
1838
- });
1839
- ctx.server.get("/songRoom", async (koaCtx) => {
1840
- koaCtx.type = "html";
1841
- koaCtx.body = `
1842
- <!DOCTYPE html>
1843
- <html>
1844
- <head>
1845
- <meta charset="UTF-8">
1846
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1847
- <title>进入 KTV 房间</title>
1848
- <script src="https://cdn.tailwindcss.com"></script>
1849
- <style>
1850
- @keyframes slideUp {
1851
- from { opacity: 0; transform: translateY(20px); }
1852
- to { opacity: 1; transform: translateY(0); }
1853
- }
1854
- .animate-pop { animation: slideUp 0.5s ease-out; }
1855
- </style>
1856
- </head>
1857
- <body class="bg-slate-50 min-h-screen flex items-center justify-center p-6 text-slate-900">
1858
- <div class="w-full max-w-sm bg-white p-8 rounded-[2.5rem] shadow-xl border border-slate-100 animate-pop">
1859
- <header class="text-center mb-8">
1860
- <h1 class="text-4xl font-black text-indigo-600 mb-2">KTV Queue</h1>
1861
- <p class="text-slate-400 font-medium">输入房间号进入房间</p>
1862
- </header>
1863
-
1864
- <div class="space-y-4">
1865
- <input id="roomInput" type="text" maxlength="10"
1866
- class="w-full px-6 py-4 bg-slate-50 rounded-2xl text-center text-2xl font-bold tracking-widest outline-none focus:ring-4 focus:ring-indigo-100 transition-all border-2 border-transparent focus:border-indigo-400"
1867
- placeholder="0000" autofocus>
1868
-
1869
- <button onclick="joinRoom()"
1870
- class="w-full py-4 bg-indigo-600 text-white text-lg font-bold rounded-2xl hover:bg-indigo-700 active:scale-95 transition-all shadow-lg shadow-indigo-100">
1871
- 进入房间
1872
- </button>
1873
- </div>
1874
-
1875
- <p class="text-center text-slate-300 text-xs mt-8 uppercase tracking-widest font-bold">Powered by StarFreedomX</p>
1876
- </div>
1877
-
1878
- <script>
1879
- function joinRoom() {
1880
- const id = document.getElementById('roomInput').value.trim();
1881
- if (id) {
1882
- // 1. 获取当前页面的基础路径 (例如 "/songRoom" 或 "/")
1883
- // 2. 移除末尾可能存在的斜杠
1884
- const currentPath = window.location.pathname.replace(/\\/$/, '');
1885
-
1886
- // 3. 拼接房间号实现跳转
1887
- // 这样:
1888
- // 如果是 127.0.0.1:5140/songRoom -> 会跳到 /songRoom/1145
1889
- // 如果是 a.b.c -> 会跳到 /1145
1890
- window.location.href = \`\${currentPath}/\${id}\`;
1891
- }
1892
- }
1893
-
1894
- // 支持回车键跳转
1895
- document.getElementById('roomInput').addEventListener('keypress', (e) => {
1896
- if (e.key === 'Enter') joinRoom();
1897
- });
1898
- </script>
1899
- </body>
1900
- </html>
1901
- `;
1902
- });
1903
- }
1904
1646
  ctx.middleware(async (session, next) => {
1905
1647
  const elements = session.elements;
1906
1648
  if (cfg.openRepeat && detectControl(featureControl, session.guildId, "repeat")) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "contributors": [
5
5
  "StarFreedomX <starfreedomx@outlook.com>"
6
6
  ],
7
- "version": "0.26.4",
7
+ "version": "0.26.6",
8
8
  "main": "lib/index.js",
9
9
  "typings": "lib/index.d.ts",
10
10
  "files": [
@@ -48,8 +48,7 @@
48
48
  "http-proxy-agent": "^7.0.2",
49
49
  "https-proxy-agent": "^7.0.6",
50
50
  "mime-types": "^3.0.1",
51
- "rss-parser": "^3.13.0",
52
- "ejs": "^3.1.10"
51
+ "rss-parser": "^3.13.0"
53
52
  },
54
53
  "devDependencies": {
55
54
  "@biomejs/biome": "2.3.7",