From 0104cb9f296dd6d5cf33652d80d7fc940d505da9 Mon Sep 17 00:00:00 2001 From: Gabe Date: Sun, 12 Oct 2025 01:41:53 +0800 Subject: [PATCH] feat: add notice for subtitle --- src/config/i18n.js | 27 +++++++++ src/subtitle/YouTubeCaptionProvider.js | 78 +++++++++++++++++++++----- src/subtitle/subtitle.js | 1 + 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/src/config/i18n.js b/src/config/i18n.js index 248bc57..70dd6f7 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -1598,4 +1598,31 @@ export const I18N = { en: `Default styles reference:`, zh_TW: `認樣式參考:`, }, + subtitle_load_succeed: { + zh: `双语字幕加载成功!`, + en: `Bilingual subtitles loaded successfully!`, + zh_TW: `双语字幕加载成功!`, + }, + subtitle_load_failed: { + zh: `双语字幕加载失败!`, + en: `Failed to load bilingual subtitles!`, + zh_TW: `双语字幕加载失败!`, + }, + try_get_subtitle_data: { + zh: `尝试获取字幕数据,请稍候...`, + en: `Trying to get subtitle data, please wait...`, + zh_TW: `尝试获取字幕数据,请稍候...`, + }, + subtitle_data_processing: { + zh: `字幕数据处理中...`, + en: `Subtitle data processing...`, + zh_TW: `字幕数据处理中...`, + }, + starting_to_process_subtitle: { + zh: `开始处理字幕数据...`, + en: `Starting to process subtitle data...`, + zh_TW: `开始处理字幕数据...`, + }, }; + +export const i18n = (lang) => (key) => I18N[key]?.[lang] || ""; diff --git a/src/subtitle/YouTubeCaptionProvider.js b/src/subtitle/YouTubeCaptionProvider.js index 96546fa..37f1942 100644 --- a/src/subtitle/YouTubeCaptionProvider.js +++ b/src/subtitle/YouTubeCaptionProvider.js @@ -10,6 +10,7 @@ import { import { sleep } from "../libs/utils.js"; import { createLogoSVG } from "../libs/svg.js"; import { randomBetween } from "../libs/utils.js"; +import { i18n } from "../config"; const VIDEO_SELECT = "#container video"; const CONTORLS_SELECT = ".ytp-right-controls"; @@ -25,9 +26,13 @@ class YouTubeCaptionProvider { #ytControls = null; #isBusy = false; #fromLang = "auto"; + #notificationEl = null; + #notificationTimeout = null; + #i18n = () => ""; constructor(setting = {}) { this.#setting = setting; + this.#i18n = i18n(setting.uiLang || "zh"); } initialize() { @@ -45,7 +50,6 @@ class YouTubeCaptionProvider { if (this.#toggleButton) { this.#toggleButton.style.opacity = "0.5"; } - this.#destroyManager(); this.#doubleClick(); }, 1000); }); @@ -113,14 +117,20 @@ class YouTubeCaptionProvider { toggleButton.onclick = () => { if (this.#isBusy) { logger.info(`Youtube Provider: It's budy now...`); - return; + this.#showNotification(this.#i18n("subtitle_data_processing")); } if (!this.#enabled) { logger.info(`Youtube Provider: Feature toggled ON.`); + this.#enabled = true; + this.#toggleButton?.replaceChildren( + createLogoSVG({ isSelected: true }) + ); this.#startManager(); } else { logger.info(`Youtube Provider: Feature toggled OFF.`); + this.#enabled = false; + this.#toggleButton?.replaceChildren(createLogoSVG()); this.#destroyManager(); } }; @@ -245,7 +255,7 @@ class YouTubeCaptionProvider { logger.info("Youtube Provider is busy..."); return; } - this.#isBusy = true; // todo: 提示用户等待中 + this.#isBusy = true; try { const videoId = this.#getVideoId(); @@ -265,6 +275,8 @@ class YouTubeCaptionProvider { return; } + this.#showNotification(this.#i18n("starting_to_process_subtitle")); + const captionTracks = await this.#getCaptionTracks(videoId); const captionTrack = this.#findCaptionTrack(captionTracks); if (!captionTrack) { @@ -365,6 +377,7 @@ class YouTubeCaptionProvider { } } catch (error) { logger.warn("Youtube Provider: unknow error", error); + this.#showNotification(this.#i18n("subtitle_load_failed")); } finally { this.#isBusy = false; } @@ -386,11 +399,9 @@ class YouTubeCaptionProvider { } #startManager() { - if (this.#enabled || this.#managerInstance) { + if (this.#managerInstance) { return; } - this.#enabled = true; - this.#toggleButton?.replaceChildren(createLogoSVG({ isSelected: true })); const videoEl = document.querySelector(VIDEO_SELECT); if (!videoEl) { @@ -400,8 +411,8 @@ class YouTubeCaptionProvider { const videoId = this.#getVideoId(); if (!this.#subtitles?.length || this.#videoId !== videoId) { - // todo: 等待并给出用户提示 logger.info("Youtube Provider: No subtitles"); + this.#showNotification(this.#i18n("try_get_subtitle_data")); this.#doubleClick(); return; } @@ -416,26 +427,24 @@ class YouTubeCaptionProvider { }); this.#managerInstance.start(); + this.#showNotification(this.#i18n("subtitle_load_succeed")); + const ytCaption = document.querySelector(YT_CAPTION_SELECT); ytCaption && (ytCaption.style.display = "none"); } #destroyManager() { - if (!this.#enabled) { + if (!this.#managerInstance) { return; } - this.#enabled = false; - this.#toggleButton?.replaceChildren(createLogoSVG()); logger.info("Youtube Provider: Destroying manager..."); + this.#managerInstance.destroy(); + this.#managerInstance = null; + const ytCaption = document.querySelector(YT_CAPTION_SELECT); ytCaption && (ytCaption.style.display = "block"); - - if (this.#managerInstance) { - this.#managerInstance.destroy(); - this.#managerInstance = null; - } } #formatSubtitles(flatEvents, lang) { @@ -790,6 +799,45 @@ class YouTubeCaptionProvider { logger.info("Youtube Provider: All subtitle chunks processed."); } + + #createNotificationElement() { + const notificationEl = document.createElement("div"); + notificationEl.className = "kiss-notification"; + Object.assign(notificationEl.style, { + position: "absolute", + top: "40%", + left: "50%", + transform: "translateX(-50%)", + background: "rgba(0,0,0,0.7)", + color: "red", + padding: "0.5em 1em", + borderRadius: "4px", + zIndex: "2147483647", + opacity: "0", + transition: "opacity 0.3s ease-in-out", + pointerEvents: "none", + fontSize: "2em", + width: "50%", + textAlign: "center", + }); + + const videoEl = document.querySelector(VIDEO_SELECT); + const videoContainer = videoEl?.parentElement?.parentElement; + if (videoContainer) { + videoContainer.appendChild(notificationEl); + this.#notificationEl = notificationEl; + } + } + + #showNotification(message, duration = 3000) { + if (!this.#notificationEl) this.#createNotificationElement(); + this.#notificationEl.textContent = message; + this.#notificationEl.style.opacity = "1"; + clearTimeout(this.#notificationTimeout); + this.#notificationTimeout = setTimeout(() => { + this.#notificationEl.style.opacity = "0"; + }, duration); + } } export const YouTubeInitializer = (() => { diff --git a/src/subtitle/subtitle.js b/src/subtitle/subtitle.js index 96d8a10..95acc9c 100644 --- a/src/subtitle/subtitle.js +++ b/src/subtitle/subtitle.js @@ -34,6 +34,7 @@ export function runSubtitle({ href, setting }) { ...subtitleSetting, apiSetting, segApiSetting, + uiLang: setting.uiLang, }); } } catch (err) {