Subtitle Shield

Hide, blur, or flip YouTube and Bilibili captions so language learners listen first.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Subtitle Shield
// @name:en      Subtitle Shield
// @name:zh-CN   Subtitle Shield
// @namespace    https://github.com/phj233/subtitle-shield
// @version      0.2.0
// @description  Hide, blur, or flip YouTube and Bilibili captions so language learners listen first.
// @description:en Hide, blur, or flip YouTube and Bilibili captions so language learners listen first.
// @description:zh-CN 为 YouTube 和 Bilibili 字幕增加模糊、倒置、隐藏、延迟和暂停显示模式,帮助语言学习者先听再看。
// @license      MIT
// @homepageURL  https://github.com/phj233/subtitle-shield
// @supportURL   https://github.com/phj233/subtitle-shield/issues
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==
(function() {
	//#region src/core/i18n.ts
	var messages = {
		"en-US": {
			menuOpenSettings: "Open Subtitle Shield",
			panelTitle: "Subtitle Shield",
			closePanel: "Close",
			minimizePanel: "Minimize",
			restorePanel: "Restore settings",
			sectionGeneral: "General",
			sectionSites: "Sites",
			sectionTuning: "Tuning",
			enabled: "Enabled",
			language: "Language",
			languageAuto: "Auto",
			languageZh: "Chinese",
			languageEn: "English",
			mode: "Caption mode",
			modeBlur: "Blur",
			modeFlip: "Flip + blur",
			modeHide: "Hide",
			modePauseOnly: "Pause only",
			modeDelay: "Delay",
			revealShortcut: "Hold shortcut",
			shortcutAlt: "Alt",
			shortcutShift: "Shift",
			shortcutControl: "Control",
			shortcutMeta: "Command / Windows",
			revealOnHover: "Reveal on hover",
			showOnPause: "Reveal while paused",
			youtubeEnabled: "YouTube",
			bilibiliEnabled: "Bilibili",
			blurPx: "Blur strength",
			delayMs: "Delay",
			captionOffsetYPx: "Vertical position",
			unitPx: "px",
			unitMs: "ms",
			reset: "Reset",
			done: "Done"
		},
		"zh-CN": {
			menuOpenSettings: "打开 Subtitle Shield",
			panelTitle: "Subtitle Shield",
			closePanel: "关闭",
			minimizePanel: "最小化",
			restorePanel: "还原设置",
			sectionGeneral: "通用",
			sectionSites: "站点",
			sectionTuning: "调节",
			enabled: "启用",
			language: "语言",
			languageAuto: "自动",
			languageZh: "中文",
			languageEn: "英文",
			mode: "字幕模式",
			modeBlur: "模糊",
			modeFlip: "倒置 + 模糊",
			modeHide: "隐藏",
			modePauseOnly: "仅暂停",
			modeDelay: "延迟",
			revealShortcut: "按住快捷键",
			shortcutAlt: "Alt",
			shortcutShift: "Shift",
			shortcutControl: "Control",
			shortcutMeta: "Command / Windows",
			revealOnHover: "悬停显示",
			showOnPause: "暂停时显示",
			youtubeEnabled: "YouTube",
			bilibiliEnabled: "Bilibili",
			blurPx: "模糊强度",
			delayMs: "延迟时间",
			captionOffsetYPx: "字幕垂直位置",
			unitPx: "像素",
			unitMs: "毫秒",
			reset: "重置",
			done: "完成"
		}
	};
	function createTranslator(languagePreference, navigatorLanguage = globalThis.navigator?.language) {
		const language = resolveLanguage(languagePreference, navigatorLanguage);
		return {
			language,
			t: (key) => readMessage(language, key) ?? readMessage("en-US", key) ?? key
		};
	}
	function resolveLanguage(languagePreference, navigatorLanguage = "") {
		if (languagePreference !== "auto") return languagePreference;
		return navigatorLanguage.toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
	}
	function readMessage(language, key) {
		return messages[language][key];
	}
	//#endregion
	//#region src/core/types.ts
	var CAPTION_MODES = [
		"blur",
		"flip",
		"hide",
		"pauseOnly",
		"delay"
	];
	var LANGUAGE_PREFERENCES = [
		"auto",
		"zh-CN",
		"en-US"
	];
	var REVEAL_SHORTCUTS = [
		"Alt",
		"Shift",
		"Control",
		"Meta"
	];
	var SITE_NAMES = ["youtube", "bilibili"];
	//#endregion
	//#region src/core/settings.ts
	var SETTINGS_STORAGE_KEY = "subtitleShield.settings.v1";
	var DEFAULT_SETTINGS = {
		schemaVersion: 1,
		enabled: true,
		language: "auto",
		mode: "blur",
		blurPx: 6,
		delayMs: 2e3,
		captionOffsetYPx: 0,
		revealShortcut: "Alt",
		revealOnHover: false,
		showOnPause: true,
		sites: {
			youtube: true,
			bilibili: true
		}
	};
	function createSettingsStore(storage) {
		let current = DEFAULT_SETTINGS;
		const listeners = /* @__PURE__ */ new Set();
		const emit = () => {
			for (const listener of listeners) listener(current);
		};
		const persist = async (settings) => {
			current = normalizeSettings(settings);
			await storage.set(SETTINGS_STORAGE_KEY, current);
			emit();
			return current;
		};
		return {
			get: () => current,
			load: async () => {
				current = normalizeSettings(await storage.get(SETTINGS_STORAGE_KEY, DEFAULT_SETTINGS));
				await storage.set(SETTINGS_STORAGE_KEY, current);
				emit();
				return current;
			},
			reset: () => persist(DEFAULT_SETTINGS),
			set: persist,
			update: (patch) => persist(mergeSettings(current, patch)),
			subscribe: (listener) => {
				listeners.add(listener);
				return () => listeners.delete(listener);
			}
		};
	}
	function mergeSettings(settings, patch) {
		return normalizeSettings({
			...settings,
			...patch,
			sites: {
				...settings.sites,
				...patch.sites
			}
		});
	}
	function normalizeSettings(value) {
		const input = isRecord(value) ? value : {};
		const sitesInput = isRecord(input.sites) ? input.sites : {};
		return {
			schemaVersion: 1,
			enabled: pickBoolean(input.enabled, DEFAULT_SETTINGS.enabled),
			language: pickOption(input.language, LANGUAGE_PREFERENCES, DEFAULT_SETTINGS.language),
			mode: pickOption(input.mode, CAPTION_MODES, DEFAULT_SETTINGS.mode),
			blurPx: clampNumber(input.blurPx, 0, 20, DEFAULT_SETTINGS.blurPx),
			delayMs: clampNumber(input.delayMs, 0, 1e4, DEFAULT_SETTINGS.delayMs),
			captionOffsetYPx: clampNumber(input.captionOffsetYPx, -120, 120, DEFAULT_SETTINGS.captionOffsetYPx),
			revealShortcut: pickOption(input.revealShortcut, REVEAL_SHORTCUTS, DEFAULT_SETTINGS.revealShortcut),
			revealOnHover: pickBoolean(input.revealOnHover, DEFAULT_SETTINGS.revealOnHover),
			showOnPause: pickBoolean(input.showOnPause, DEFAULT_SETTINGS.showOnPause),
			sites: SITE_NAMES.reduce((result, site) => {
				result[site] = pickBoolean(sitesInput[site], DEFAULT_SETTINGS.sites[site]);
				return result;
			}, {})
		};
	}
	function isRecord(value) {
		return typeof value === "object" && value !== null && !Array.isArray(value);
	}
	function pickBoolean(value, fallback) {
		return typeof value === "boolean" ? value : fallback;
	}
	function pickOption(value, options, fallback) {
		return typeof value === "string" && options.includes(value) ? value : fallback;
	}
	function clampNumber(value, min, max, fallback) {
		if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
		return Math.min(max, Math.max(min, Math.round(value)));
	}
	//#endregion
	//#region src/core/shortcuts.ts
	function createShortcutController({ getSettings, onRevealChange }) {
		let activeShortcut = null;
		let settings = getSettings();
		const setRevealing = (revealing) => {
			onRevealChange(revealing);
		};
		const reset = () => {
			if (activeShortcut === null) return;
			activeShortcut = null;
			setRevealing(false);
		};
		const handleKeyDown = (event) => {
			if (event.repeat || isEditableTarget(event.target)) return;
			const shortcut = settings.revealShortcut;
			if (event.key === shortcut) {
				activeShortcut = shortcut;
				setRevealing(true);
			}
		};
		const handleKeyUp = (event) => {
			if (activeShortcut !== null && event.key === activeShortcut) reset();
		};
		const handleVisibilityChange = () => {
			if (document.visibilityState !== "visible") reset();
		};
		window.addEventListener("keydown", handleKeyDown, true);
		window.addEventListener("keyup", handleKeyUp, true);
		window.addEventListener("blur", reset);
		document.addEventListener("visibilitychange", handleVisibilityChange);
		return {
			setSettings: (nextSettings) => {
				if (settings.revealShortcut !== nextSettings.revealShortcut) reset();
				settings = nextSettings;
			},
			destroy: () => {
				reset();
				window.removeEventListener("keydown", handleKeyDown, true);
				window.removeEventListener("keyup", handleKeyUp, true);
				window.removeEventListener("blur", reset);
				document.removeEventListener("visibilitychange", handleVisibilityChange);
			}
		};
	}
	function isEditableTarget(target) {
		if (!(target instanceof HTMLElement)) return false;
		return target.isContentEditable || target.matches("input, textarea, select, [contenteditable=\"true\"]");
	}
	//#endregion
	//#region src/core/styles.ts
	var STYLE_ID = "subtitle-shield-style";
	var SUBTITLE_SHIELD_STYLESHEET = `
.ss-caption-root {
  --ss-blur-px: 6px;
  --ss-offset-y: 0px;
  translate: 0 var(--ss-offset-y);
  transition:
    filter 150ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 150ms cubic-bezier(0.22, 1, 0.36, 1),
    rotate 150ms cubic-bezier(0.22, 1, 0.36, 1),
    translate 150ms cubic-bezier(0.22, 1, 0.36, 1);
}

.ss-caption-root.ss-enabled.ss-mode-blur:not(.ss-revealing):not(.ss-video-paused) {
  filter: blur(var(--ss-blur-px)) !important;
  opacity: 0.82 !important;
}

.ss-caption-root.ss-enabled.ss-mode-flip:not(.ss-revealing):not(.ss-video-paused) {
  filter: blur(var(--ss-blur-px)) !important;
  opacity: 0.82 !important;
  rotate: 180deg !important;
}

.ss-caption-root.ss-enabled.ss-mode-hide:not(.ss-revealing):not(.ss-video-paused),
.ss-caption-root.ss-enabled.ss-mode-pause-only:not(.ss-revealing):not(.ss-video-paused),
.ss-caption-root.ss-enabled.ss-mode-delay:not(.ss-revealing):not(.ss-video-paused):not(.ss-delay-ready) {
  opacity: 0 !important;
  visibility: hidden !important;
}

.ss-caption-root.ss-hover-reveal:hover,
.ss-caption-root.ss-revealing,
.ss-caption-root.ss-video-paused {
  filter: none !important;
  opacity: 1 !important;
  rotate: none !important;
  visibility: visible !important;
}

.ss-panel-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 999999;
}

.ss-panel {
  position: fixed;
  top: 72px;
  right: 16px;
  width: min(372px, calc(100vw - 32px));
  max-height: calc(100vh - 96px);
  overflow: auto;
  box-sizing: border-box;
  pointer-events: auto;
  color: oklch(22% 0.012 245);
  background: oklch(98% 0.006 245);
  border: 1px solid oklch(86% 0.018 245);
  border-radius: 8px;
  box-shadow: 0 18px 48px oklch(22% 0.012 245 / 18%);
  font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

.ss-panel,
.ss-panel * {
  box-sizing: border-box;
}

.ss-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 14px 10px;
  border-bottom: 1px solid oklch(90% 0.012 245);
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.ss-panel--dragging .ss-panel__header {
  cursor: grabbing;
}

.ss-panel__title {
  margin: 0;
  color: oklch(20% 0.016 245);
  font-size: 15px;
  font-weight: 650;
  line-height: 1.2;
}

.ss-panel__actions {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.ss-panel__icon-button {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  color: oklch(38% 0.018 245);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  font: inherit;
  cursor: pointer;
  touch-action: manipulation;
}

.ss-panel__icon-button:hover {
  background: oklch(94% 0.01 245);
  border-color: oklch(88% 0.014 245);
}

.ss-panel__close:focus-visible,
.ss-panel button:focus-visible,
.ss-panel input:focus-visible,
.ss-panel select:focus-visible {
  outline: 2px solid oklch(56% 0.14 230);
  outline-offset: 2px;
}

.ss-panel__body {
  display: grid;
  gap: 14px;
  padding: 14px;
}

.ss-panel__section {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.ss-panel__legend {
  margin: 0 0 8px;
  padding: 0;
  color: oklch(40% 0.018 245);
  font-size: 12px;
  font-weight: 650;
}

.ss-panel__stack {
  display: grid;
  gap: 10px;
}

.ss-panel__row,
.ss-panel__check {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.ss-panel__label {
  color: oklch(24% 0.014 245);
  font-size: 13px;
  font-weight: 520;
}

.ss-panel__check {
  justify-content: flex-start;
}

.ss-panel__check input {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: oklch(56% 0.14 230);
}

.ss-panel select,
.ss-panel input[type="number"] {
  min-width: 132px;
  min-height: 32px;
  padding: 5px 8px;
  color: oklch(22% 0.012 245);
  background: oklch(100% 0.003 245);
  border: 1px solid oklch(82% 0.018 245);
  border-radius: 6px;
  font: inherit;
}

.ss-panel input[type="range"] {
  width: 136px;
  accent-color: oklch(56% 0.14 230);
}

.ss-panel__number {
  display: flex;
  align-items: center;
  gap: 6px;
}

.ss-panel__number input[type="number"] {
  min-width: 84px;
  width: 84px;
}

.ss-panel__unit {
  min-width: 32px;
  color: oklch(46% 0.016 245);
  font-size: 12px;
}

.ss-panel__footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 14px 14px;
  border-top: 1px solid oklch(90% 0.012 245);
}

.ss-panel__button {
  min-height: 32px;
  padding: 5px 11px;
  color: oklch(22% 0.012 245);
  background: oklch(100% 0.003 245);
  border: 1px solid oklch(82% 0.018 245);
  border-radius: 6px;
  font: inherit;
  font-weight: 560;
  cursor: pointer;
}

.ss-panel__button:hover {
  background: oklch(94% 0.01 245);
}

.ss-panel__button--primary {
  color: oklch(99% 0.004 245);
  background: oklch(48% 0.14 230);
  border-color: oklch(48% 0.14 230);
}

.ss-panel__button--primary:hover {
  background: oklch(43% 0.14 230);
}

.ss-panel-ball {
  position: fixed;
  right: 16px;
  bottom: 16px;
  display: inline-grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  pointer-events: auto;
  color: oklch(99% 0.004 245);
  background: oklch(48% 0.14 230);
  border: 1px solid oklch(58% 0.13 230);
  border-radius: 999px;
  box-shadow: 0 14px 34px oklch(22% 0.012 245 / 22%);
  font: 700 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  letter-spacing: 0;
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.ss-panel-ball:hover {
  background: oklch(43% 0.14 230);
}

.ss-panel-ball:focus-visible {
  outline: 2px solid oklch(56% 0.14 230);
  outline-offset: 3px;
}

.ss-panel-ball--dragging {
  cursor: grabbing;
}

@media (max-width: 520px) {
  .ss-panel {
    top: 12px;
    right: 12px;
    width: calc(100vw - 24px);
    max-height: calc(100vh - 24px);
  }

  .ss-panel__row {
    align-items: flex-start;
    flex-direction: column;
    gap: 6px;
  }
}
`;
	function injectStyles(documentRef = document) {
		if (documentRef.getElementById("subtitle-shield-style")) return;
		const style = documentRef.createElement("style");
		style.id = STYLE_ID;
		style.textContent = SUBTITLE_SHIELD_STYLESHEET;
		documentRef.head.append(style);
	}
	//#endregion
	//#region \0@[email protected]/helpers/esm/typeof.js
	function _typeof(o) {
		"@babel/helpers - typeof";
		return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
			return typeof o;
		} : function(o) {
			return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
		}, _typeof(o);
	}
	//#endregion
	//#region \0@[email protected]/helpers/esm/toPrimitive.js
	function toPrimitive(t, r) {
		if ("object" != _typeof(t) || !t) return t;
		var e = t[Symbol.toPrimitive];
		if (void 0 !== e) {
			var i = e.call(t, r || "default");
			if ("object" != _typeof(i)) return i;
			throw new TypeError("@@toPrimitive must return a primitive value.");
		}
		return ("string" === r ? String : Number)(t);
	}
	//#endregion
	//#region \0@[email protected]/helpers/esm/toPropertyKey.js
	function toPropertyKey(t) {
		var i = toPrimitive(t, "string");
		return "symbol" == _typeof(i) ? i : i + "";
	}
	//#endregion
	//#region \0@[email protected]/helpers/esm/defineProperty.js
	function _defineProperty(e, r, t) {
		return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
			value: t,
			enumerable: !0,
			configurable: !0,
			writable: !0
		}) : e[r] = t, e;
	}
	//#endregion
	//#region src/core/subtitle-engine.ts
	var MODE_CLASSES = [
		"ss-mode-blur",
		"ss-mode-flip",
		"ss-mode-hide",
		"ss-mode-pause-only",
		"ss-mode-delay"
	];
	var ROOT_CLASSES = [
		"ss-caption-root",
		"ss-enabled",
		"ss-revealing",
		"ss-video-paused",
		"ss-delay-ready",
		"ss-hover-reveal",
		...MODE_CLASSES
	];
	var SubtitleEngine = class {
		constructor(settings = DEFAULT_SETTINGS) {
			_defineProperty(this, "settings", DEFAULT_SETTINGS);
			_defineProperty(this, "snapshot", null);
			_defineProperty(this, "targets", /* @__PURE__ */ new Set());
			_defineProperty(this, "delayStates", /* @__PURE__ */ new Map());
			_defineProperty(this, "revealing", false);
			_defineProperty(this, "video", null);
			_defineProperty(this, "handleVideoStateChange", () => {
				this.apply();
			});
			this.settings = settings;
		}
		setSettings(settings) {
			this.settings = settings;
			this.apply();
		}
		setSnapshot(snapshot) {
			this.snapshot = snapshot;
			this.setVideo(snapshot.video);
			this.replaceTargets(snapshot.captionContainers);
			this.apply();
		}
		setRevealing(revealing) {
			if (this.revealing === revealing) return;
			this.revealing = revealing;
			this.apply();
		}
		destroy() {
			for (const target of this.targets) this.cleanTarget(target);
			this.targets.clear();
			this.setVideo(null);
		}
		replaceTargets(nextTargets) {
			const next = new Set(nextTargets);
			for (const target of this.targets) if (!next.has(target)) {
				this.cleanTarget(target);
				this.targets.delete(target);
			}
			for (const target of next) this.targets.add(target);
		}
		setVideo(video) {
			if (this.video === video) return;
			this.video?.removeEventListener("play", this.handleVideoStateChange);
			this.video?.removeEventListener("pause", this.handleVideoStateChange);
			this.video?.removeEventListener("ended", this.handleVideoStateChange);
			this.video = video;
			this.video?.addEventListener("play", this.handleVideoStateChange);
			this.video?.addEventListener("pause", this.handleVideoStateChange);
			this.video?.addEventListener("ended", this.handleVideoStateChange);
		}
		apply() {
			const snapshot = this.snapshot;
			const siteEnabled = snapshot ? this.settings.sites[snapshot.site] : false;
			const active = this.settings.enabled && siteEnabled;
			const isPausedReveal = Boolean(this.video?.paused) && (this.settings.showOnPause || this.settings.mode === "pauseOnly");
			for (const target of this.targets) {
				if (!active) {
					this.cleanTarget(target);
					continue;
				}
				target.classList.add("ss-caption-root", "ss-enabled");
				target.classList.remove(...MODE_CLASSES);
				target.classList.add(modeClassName(this.settings.mode));
				target.classList.toggle("ss-revealing", this.revealing);
				target.classList.toggle("ss-video-paused", isPausedReveal);
				target.classList.toggle("ss-hover-reveal", this.settings.revealOnHover);
				target.style.setProperty("--ss-blur-px", `${this.settings.blurPx}px`);
				target.style.setProperty("--ss-offset-y", `${this.settings.captionOffsetYPx}px`);
				this.updateDelayTracking(target, this.settings.mode === "delay");
			}
		}
		updateDelayTracking(target, shouldTrack) {
			if (!shouldTrack) {
				this.teardownDelay(target);
				target.classList.remove("ss-delay-ready");
				return;
			}
			if (this.delayStates.has(target)) {
				const state = this.delayStates.get(target);
				if (state && state.delayMs !== this.settings.delayMs) {
					state.delayMs = this.settings.delayMs;
					this.restartDelay(target);
				}
				return;
			}
			const observer = new MutationObserver(() => this.restartDelay(target));
			observer.observe(target, {
				childList: true,
				characterData: true,
				subtree: true
			});
			this.delayStates.set(target, {
				observer,
				timer: null,
				delayMs: this.settings.delayMs
			});
			this.restartDelay(target);
		}
		restartDelay(target) {
			const state = this.delayStates.get(target);
			if (!state) return;
			if (state.timer !== null) {
				window.clearTimeout(state.timer);
				state.timer = null;
			}
			target.classList.remove("ss-delay-ready");
			if (!target.textContent?.trim() || this.settings.delayMs <= 0) {
				target.classList.add("ss-delay-ready");
				return;
			}
			state.timer = window.setTimeout(() => {
				target.classList.add("ss-delay-ready");
				state.timer = null;
			}, this.settings.delayMs);
		}
		teardownDelay(target) {
			const state = this.delayStates.get(target);
			if (!state) return;
			if (state.timer !== null) window.clearTimeout(state.timer);
			state.observer.disconnect();
			this.delayStates.delete(target);
		}
		cleanTarget(target) {
			this.teardownDelay(target);
			target.classList.remove(...ROOT_CLASSES);
			target.style.removeProperty("--ss-blur-px");
			target.style.removeProperty("--ss-offset-y");
		}
	};
	function modeClassName(mode) {
		switch (mode) {
			case "blur": return "ss-mode-blur";
			case "flip": return "ss-mode-flip";
			case "hide": return "ss-mode-hide";
			case "pauseOnly": return "ss-mode-pause-only";
			case "delay": return "ss-mode-delay";
		}
	}
	//#endregion
	//#region src/sites/dom.ts
	function toHtmlElements(elements) {
		const result = [];
		for (const element of elements) if (element instanceof HTMLElement) result.push(element);
		return uniqueElements(result);
	}
	function uniqueElements(elements) {
		return [...new Set(elements)];
	}
	function queryHtmlElements(root, selectors) {
		return uniqueElements(selectors.flatMap((selector) => toHtmlElements(root.querySelectorAll(selector))));
	}
	//#endregion
	//#region src/sites/navigation.ts
	var listeners = /* @__PURE__ */ new Set();
	var installed = false;
	function watchNavigation(listener) {
		listeners.add(listener);
		installNavigationWatcher();
		return () => {
			listeners.delete(listener);
		};
	}
	function installNavigationWatcher() {
		if (installed) return;
		installed = true;
		const originalPushState = history.pushState;
		const originalReplaceState = history.replaceState;
		history.pushState = function pushState(...args) {
			const result = originalPushState.apply(this, args);
			queueNavigationEmit();
			return result;
		};
		history.replaceState = function replaceState(...args) {
			const result = originalReplaceState.apply(this, args);
			queueNavigationEmit();
			return result;
		};
		window.addEventListener("popstate", queueNavigationEmit);
		window.addEventListener("hashchange", queueNavigationEmit);
	}
	var queued = false;
	function queueNavigationEmit() {
		if (queued) return;
		queued = true;
		window.setTimeout(() => {
			queued = false;
			for (const listener of listeners) listener();
		}, 0);
	}
	//#endregion
	//#region src/sites/bilibili.ts
	var CAPTION_SELECTORS = [
		".bpx-player-subtitle-panel",
		".bpx-player-subtitle-wrap",
		".bpx-player-subtitle-item",
		".bilibili-player-video-subtitle",
		".bilibili-player-video-subtitle-panel"
	];
	var FALLBACK_BLOCKLIST = [
		"button",
		"control",
		"danmaku",
		"dm",
		"setting",
		"switch",
		"tooltip"
	];
	function createBilibiliAdapter() {
		return {
			site: "bilibili",
			matches: (location) => location.hostname === "www.bilibili.com" && (location.pathname.startsWith("/video/") || location.pathname.startsWith("/list/")),
			setup: (onSnapshot) => setupBilibiliAdapter(onSnapshot)
		};
	}
	function setupBilibiliAdapter(onSnapshot) {
		let animationFrame = 0;
		let observedRoot = null;
		const observer = new MutationObserver(() => scheduleRefresh());
		const scheduleRefresh = () => {
			if (animationFrame !== 0) return;
			animationFrame = window.requestAnimationFrame(() => {
				animationFrame = 0;
				refresh();
			});
		};
		const refresh = () => {
			const snapshot = getBilibiliSnapshot();
			onSnapshot(snapshot);
			const nextRoot = snapshot.player ?? document.querySelector("#app") ?? document.body;
			if (nextRoot !== observedRoot) {
				observer.disconnect();
				observer.observe(nextRoot, {
					childList: true,
					subtree: true,
					characterData: true
				});
				observedRoot = nextRoot;
			}
		};
		const unwatchNavigation = watchNavigation(scheduleRefresh);
		document.addEventListener("visibilitychange", scheduleRefresh);
		refresh();
		return {
			refresh,
			destroy: () => {
				if (animationFrame !== 0) window.cancelAnimationFrame(animationFrame);
				observer.disconnect();
				unwatchNavigation();
				document.removeEventListener("visibilitychange", scheduleRefresh);
			}
		};
	}
	function getBilibiliSnapshot() {
		const player = findBilibiliPlayer();
		const root = player ?? document;
		return {
			site: "bilibili",
			player,
			video: player?.querySelector("video") ?? document.querySelector("video"),
			captionContainers: findBilibiliCaptionContainers(root)
		};
	}
	function findBilibiliPlayer() {
		const video = document.querySelector("video");
		return document.querySelector(".bpx-player-container") ?? document.querySelector("#bilibili-player") ?? document.querySelector(".bilibili-player") ?? video?.closest("[class*=\"player\"]") ?? null;
	}
	function findBilibiliCaptionContainers(root) {
		const stableTargets = queryHtmlElements(root, CAPTION_SELECTORS);
		if (stableTargets.length > 0) return stableTargets;
		return uniqueElements(toHtmlElements(root.querySelectorAll("[class*=\"subtitle\"]")).filter(isLikelyCaptionElement));
	}
	function isLikelyCaptionElement(element) {
		const className = element.className.toString().toLowerCase();
		if (!className.includes("subtitle")) return false;
		return !FALLBACK_BLOCKLIST.some((blocked) => className.includes(blocked));
	}
	//#endregion
	//#region src/sites/youtube.ts
	function createYoutubeAdapter() {
		return {
			site: "youtube",
			matches: (location) => location.hostname === "youtube.com" || location.hostname.endsWith(".youtube.com"),
			setup: (onSnapshot) => setupYoutubeAdapter(onSnapshot)
		};
	}
	function setupYoutubeAdapter(onSnapshot) {
		let animationFrame = 0;
		let observedRoot = null;
		const observer = new MutationObserver(() => scheduleRefresh());
		const scheduleRefresh = () => {
			if (animationFrame !== 0) return;
			animationFrame = window.requestAnimationFrame(() => {
				animationFrame = 0;
				refresh();
			});
		};
		const refresh = () => {
			const snapshot = getYoutubeSnapshot();
			onSnapshot(snapshot);
			const nextRoot = snapshot.player ?? document.querySelector("ytd-app") ?? document.body;
			if (nextRoot !== observedRoot) {
				observer.disconnect();
				observer.observe(nextRoot, {
					childList: true,
					subtree: true,
					characterData: true
				});
				observedRoot = nextRoot;
			}
		};
		const unwatchNavigation = watchNavigation(scheduleRefresh);
		window.addEventListener("yt-navigate-finish", scheduleRefresh);
		document.addEventListener("visibilitychange", scheduleRefresh);
		refresh();
		return {
			refresh,
			destroy: () => {
				if (animationFrame !== 0) window.cancelAnimationFrame(animationFrame);
				observer.disconnect();
				unwatchNavigation();
				window.removeEventListener("yt-navigate-finish", scheduleRefresh);
				document.removeEventListener("visibilitychange", scheduleRefresh);
			}
		};
	}
	function getYoutubeSnapshot() {
		const player = findYoutubePlayer();
		const root = player ?? document;
		return {
			site: "youtube",
			player,
			video: player?.querySelector("video") ?? document.querySelector("video"),
			captionContainers: findYoutubeCaptionContainers(root)
		};
	}
	function findYoutubePlayer() {
		return document.querySelector(".html5-video-player") ?? document.querySelector("#movie_player") ?? document.querySelector("ytd-player");
	}
	function findYoutubeCaptionContainers(root) {
		const windows = queryHtmlElements(root, [".caption-window"]);
		if (windows.length > 0) return windows;
		const segments = queryHtmlElements(root, [".ytp-caption-window-container .ytp-caption-segment"]);
		if (segments.length > 0) return segments;
		return uniqueElements(toHtmlElements(root.querySelectorAll(".ytp-caption-window-container")).filter((element) => element.textContent?.trim()));
	}
	//#endregion
	//#region src/sites/index.ts
	function detectSiteAdapter(location = window.location) {
		return [createYoutubeAdapter(), createBilibiliAdapter()].find((adapter) => adapter.matches(location)) ?? null;
	}
	//#endregion
	//#region src/ui/settings-panel.ts
	function createSettingsPanel(store) {
		let layer = null;
		let renderedLanguage = "";
		let panelPosition = null;
		let ballPosition = null;
		let dragState = null;
		let minimized = false;
		const unsubscribe = store.subscribe((settings) => {
			if (layer && settings.language !== renderedLanguage) render();
		});
		const handleResize = () => {
			if (!layer) return;
			if (minimized && ballPosition) {
				const ball = layer.querySelector(".ss-panel-ball");
				if (ball) {
					ballPosition = clampElementPosition(ball, ballPosition.left, ballPosition.top);
					applyElementPosition(ball, ballPosition);
				}
			}
			if (!minimized && panelPosition) {
				const panel = layer.querySelector(".ss-panel");
				if (panel) {
					panelPosition = clampElementPosition(panel, panelPosition.left, panelPosition.top);
					applyElementPosition(panel, panelPosition);
				}
			}
		};
		const handleEscape = (event) => {
			if (event.key === "Escape") close();
		};
		const close = () => {
			endDrag();
			layer?.remove();
			layer = null;
			document.removeEventListener("keydown", handleEscape);
			window.removeEventListener("resize", handleResize);
		};
		const open = () => {
			if (layer) {
				if (minimized) {
					restorePanel();
					return;
				}
				layer.querySelector(".ss-panel__minimize")?.focus();
				return;
			}
			layer = document.createElement("div");
			layer.className = "ss-panel-layer";
			document.body.append(layer);
			document.addEventListener("keydown", handleEscape);
			window.addEventListener("resize", handleResize);
			render();
			layer.querySelector(".ss-panel__close")?.focus();
		};
		const render = () => {
			if (!layer) return;
			if (minimized) {
				renderBall();
				return;
			}
			const settings = store.get();
			const { t, language } = createTranslator(settings.language);
			renderedLanguage = settings.language;
			const panel = document.createElement("section");
			panel.className = "ss-panel";
			panel.setAttribute("role", "dialog");
			panel.setAttribute("aria-modal", "false");
			panel.setAttribute("aria-labelledby", "ss-panel-title");
			const title = document.createElement("h2");
			title.id = "ss-panel-title";
			title.className = "ss-panel__title";
			title.textContent = t("panelTitle");
			const closeButton = document.createElement("button");
			closeButton.className = "ss-panel__icon-button ss-panel__close";
			closeButton.type = "button";
			closeButton.textContent = "×";
			closeButton.setAttribute("aria-label", t("closePanel"));
			closeButton.addEventListener("click", close);
			const minimizeButton = document.createElement("button");
			minimizeButton.className = "ss-panel__icon-button ss-panel__minimize";
			minimizeButton.type = "button";
			minimizeButton.textContent = "–";
			minimizeButton.setAttribute("aria-label", t("minimizePanel"));
			minimizeButton.addEventListener("click", minimizePanel);
			const actions = document.createElement("div");
			actions.className = "ss-panel__actions";
			actions.append(minimizeButton, closeButton);
			const header = document.createElement("header");
			header.className = "ss-panel__header";
			header.addEventListener("pointerdown", (event) => startDrag(event, panel));
			header.append(title, actions);
			const body = document.createElement("div");
			body.className = "ss-panel__body";
			body.append(createSection(t("sectionGeneral"), [
				createCheckbox(t("enabled"), settings.enabled, (checked) => store.update({ enabled: checked })),
				createSelect(t("language"), settings.language, LANGUAGE_PREFERENCES, (value) => languageLabel(value, t), (value) => store.update({ language: value })),
				createSelect(t("mode"), settings.mode, CAPTION_MODES, (value) => modeLabel(value, t), (value) => store.update({ mode: value })),
				createSelect(t("revealShortcut"), settings.revealShortcut, REVEAL_SHORTCUTS, (value) => shortcutLabel(value, t), (value) => store.update({ revealShortcut: value })),
				createCheckbox(t("revealOnHover"), settings.revealOnHover, (checked) => store.update({ revealOnHover: checked })),
				createCheckbox(t("showOnPause"), settings.showOnPause, (checked) => store.update({ showOnPause: checked }))
			]), createSection(t("sectionSites"), [createCheckbox(t("youtubeEnabled"), settings.sites.youtube, (checked) => updateSite("youtube", checked)), createCheckbox(t("bilibiliEnabled"), settings.sites.bilibili, (checked) => updateSite("bilibili", checked))]), createSection(t("sectionTuning"), [
				createRangeNumberRow({
					label: t("blurPx"),
					unit: t("unitPx"),
					value: settings.blurPx,
					min: 0,
					max: 20,
					step: 1,
					onValue: (value) => store.update({ blurPx: value })
				}),
				createNumberRow({
					label: t("delayMs"),
					unit: t("unitMs"),
					value: settings.delayMs,
					min: 0,
					max: 1e4,
					step: 100,
					onValue: (value) => store.update({ delayMs: value })
				}),
				createRangeNumberRow({
					label: t("captionOffsetYPx"),
					unit: t("unitPx"),
					value: settings.captionOffsetYPx,
					min: -120,
					max: 120,
					step: 1,
					onValue: (value) => store.update({ captionOffsetYPx: value })
				})
			]));
			const resetButton = document.createElement("button");
			resetButton.className = "ss-panel__button";
			resetButton.type = "button";
			resetButton.textContent = t("reset");
			resetButton.addEventListener("click", () => {
				store.reset().then(render);
			});
			const doneButton = document.createElement("button");
			doneButton.className = "ss-panel__button ss-panel__button--primary";
			doneButton.type = "button";
			doneButton.textContent = t("done");
			doneButton.addEventListener("click", close);
			const footer = document.createElement("footer");
			footer.className = "ss-panel__footer";
			footer.append(resetButton, doneButton);
			panel.lang = language;
			panel.append(header, body, footer);
			if (panelPosition) applyElementPosition(panel, panelPosition);
			layer.replaceChildren(panel);
		};
		const renderBall = () => {
			if (!layer) return;
			const { t, language } = createTranslator(store.get().language);
			renderedLanguage = store.get().language;
			const ball = document.createElement("button");
			ball.className = "ss-panel-ball";
			ball.type = "button";
			ball.textContent = "SS";
			ball.lang = language;
			ball.setAttribute("aria-label", t("restorePanel"));
			ball.addEventListener("pointerdown", (event) => startDrag(event, ball));
			ball.addEventListener("click", (event) => {
				if (event.detail === 0) restorePanel();
			});
			if (!ballPosition) ballPosition = getDefaultBallPosition();
			applyElementPosition(ball, ballPosition);
			layer.replaceChildren(ball);
			ball.focus();
		};
		const minimizePanel = () => {
			if (!layer) return;
			const panel = layer.querySelector(".ss-panel");
			if (panel) {
				const rect = panel.getBoundingClientRect();
				panelPosition = {
					left: rect.left,
					top: rect.top
				};
				if (!ballPosition) ballPosition = clampElementPosition(getVirtualBallElement(), rect.right - FLOATING_BALL_SIZE, rect.top);
			}
			minimized = true;
			render();
		};
		const restorePanel = () => {
			minimized = false;
			render();
			layer?.querySelector(".ss-panel__minimize")?.focus();
		};
		const startDrag = (event, element) => {
			const isBall = element.classList.contains("ss-panel-ball");
			if (event.button !== 0 || !isBall && isInteractiveTarget(event.target)) return;
			const rect = element.getBoundingClientRect();
			dragState = {
				element,
				kind: element.classList.contains("ss-panel-ball") ? "ball" : "panel",
				pointerId: event.pointerId,
				offsetX: event.clientX - rect.left,
				offsetY: event.clientY - rect.top,
				startX: event.clientX,
				startY: event.clientY,
				didMove: false
			};
			element.classList.add(dragState.kind === "ball" ? "ss-panel-ball--dragging" : "ss-panel--dragging");
			element.setPointerCapture(event.pointerId);
			element.addEventListener("pointermove", handleDragMove);
			element.addEventListener("pointerup", handleDragEnd);
			element.addEventListener("pointercancel", handleDragEnd);
			event.preventDefault();
		};
		const handleDragMove = (event) => {
			if (!dragState || event.pointerId !== dragState.pointerId) return;
			dragState.didMove = dragState.didMove || Math.hypot(event.clientX - dragState.startX, event.clientY - dragState.startY) > 4;
			const nextPosition = clampElementPosition(dragState.element, event.clientX - dragState.offsetX, event.clientY - dragState.offsetY);
			if (dragState.kind === "ball") ballPosition = nextPosition;
			else panelPosition = nextPosition;
			applyElementPosition(dragState.element, nextPosition);
		};
		const handleDragEnd = (event) => {
			if (!dragState || event.pointerId !== dragState.pointerId) return;
			const shouldRestoreBall = dragState.kind === "ball" && !dragState.didMove && event.type === "pointerup";
			endDrag();
			if (shouldRestoreBall) restorePanel();
		};
		const endDrag = () => {
			if (!dragState) return;
			const { element, kind, pointerId } = dragState;
			element.classList.remove(kind === "ball" ? "ss-panel-ball--dragging" : "ss-panel--dragging");
			element.removeEventListener("pointermove", handleDragMove);
			element.removeEventListener("pointerup", handleDragEnd);
			element.removeEventListener("pointercancel", handleDragEnd);
			try {
				element.releasePointerCapture(pointerId);
			} catch {}
			dragState = null;
		};
		const updateSite = (site, enabled) => store.update({ sites: { [site]: enabled } });
		return {
			open,
			close,
			destroy: () => {
				close();
				unsubscribe();
			}
		};
	}
	var FLOATING_BALL_SIZE = 48;
	function applyElementPosition(element, position) {
		element.style.left = `${position.left}px`;
		element.style.top = `${position.top}px`;
		element.style.right = "auto";
		element.style.bottom = "auto";
	}
	function clampElementPosition(element, left, top) {
		const margin = 8;
		const width = element.offsetWidth || FLOATING_BALL_SIZE;
		const height = element.offsetHeight || FLOATING_BALL_SIZE;
		const maxLeft = Math.max(margin, window.innerWidth - width - margin);
		const maxTop = Math.max(margin, window.innerHeight - height - margin);
		return {
			left: Math.min(maxLeft, Math.max(margin, left)),
			top: Math.min(maxTop, Math.max(margin, top))
		};
	}
	function getDefaultBallPosition() {
		const margin = 16;
		return {
			left: Math.max(8, window.innerWidth - FLOATING_BALL_SIZE - margin),
			top: Math.max(8, window.innerHeight - FLOATING_BALL_SIZE - margin)
		};
	}
	function getVirtualBallElement() {
		const element = document.createElement("div");
		element.style.width = `${FLOATING_BALL_SIZE}px`;
		element.style.height = `${FLOATING_BALL_SIZE}px`;
		return element;
	}
	function isInteractiveTarget(target) {
		return target instanceof HTMLElement && Boolean(target.closest("button, input, select, textarea, a"));
	}
	function createSection(label, rows) {
		const section = document.createElement("fieldset");
		section.className = "ss-panel__section";
		const legend = document.createElement("legend");
		legend.className = "ss-panel__legend";
		legend.textContent = label;
		const stack = document.createElement("div");
		stack.className = "ss-panel__stack";
		stack.append(...rows);
		section.append(legend, stack);
		return section;
	}
	function createCheckbox(label, checked, onChange) {
		const row = document.createElement("label");
		row.className = "ss-panel__check";
		const input = document.createElement("input");
		input.type = "checkbox";
		input.checked = checked;
		input.addEventListener("change", () => {
			onChange(input.checked);
		});
		const text = document.createElement("span");
		text.className = "ss-panel__label";
		text.textContent = label;
		row.append(input, text);
		return row;
	}
	function createSelect(label, value, options, labelFor, onChange) {
		const row = document.createElement("label");
		row.className = "ss-panel__row";
		const text = document.createElement("span");
		text.className = "ss-panel__label";
		text.textContent = label;
		const select = document.createElement("select");
		for (const optionValue of options) {
			const option = document.createElement("option");
			option.value = optionValue;
			option.textContent = labelFor(optionValue);
			select.append(option);
		}
		select.value = value;
		select.addEventListener("change", () => {
			onChange(select.value);
		});
		row.append(text, select);
		return row;
	}
	function createRangeNumberRow(options) {
		const row = document.createElement("label");
		row.className = "ss-panel__row";
		const text = document.createElement("span");
		text.className = "ss-panel__label";
		text.textContent = options.label;
		const range = document.createElement("input");
		range.type = "range";
		range.min = String(options.min);
		range.max = String(options.max);
		range.step = String(options.step);
		range.value = String(options.value);
		const numberWrap = createNumberInput(options);
		const number = numberWrap.querySelector("input");
		range.addEventListener("input", () => {
			if (number) number.value = range.value;
			options.onValue(Number(range.value));
		});
		const controls = document.createElement("div");
		controls.className = "ss-panel__number";
		controls.append(range, numberWrap);
		row.append(text, controls);
		return row;
	}
	function createNumberRow(options) {
		const row = document.createElement("label");
		row.className = "ss-panel__row";
		const text = document.createElement("span");
		text.className = "ss-panel__label";
		text.textContent = options.label;
		row.append(text, createNumberInput(options));
		return row;
	}
	function createNumberInput(options) {
		const wrap = document.createElement("span");
		wrap.className = "ss-panel__number";
		const input = document.createElement("input");
		input.type = "number";
		input.min = String(options.min);
		input.max = String(options.max);
		input.step = String(options.step);
		input.value = String(options.value);
		input.addEventListener("change", () => {
			options.onValue(Number(input.value));
		});
		const unit = document.createElement("span");
		unit.className = "ss-panel__unit";
		unit.textContent = options.unit;
		wrap.append(input, unit);
		return wrap;
	}
	function languageLabel(value, t) {
		switch (value) {
			case "auto": return t("languageAuto");
			case "zh-CN": return t("languageZh");
			case "en-US": return t("languageEn");
		}
	}
	function modeLabel(value, t) {
		switch (value) {
			case "blur": return t("modeBlur");
			case "flip": return t("modeFlip");
			case "hide": return t("modeHide");
			case "pauseOnly": return t("modePauseOnly");
			case "delay": return t("modeDelay");
		}
	}
	function shortcutLabel(value, t) {
		switch (value) {
			case "Alt": return t("shortcutAlt");
			case "Shift": return t("shortcutShift");
			case "Control": return t("shortcutControl");
			case "Meta": return t("shortcutMeta");
		}
	}
	//#endregion
	//#region src/app/bootstrap.ts
	async function bootstrapSubtitleShield({ storage, adapter, registerMenuCommand }) {
		injectStyles();
		const store = createSettingsStore(storage);
		await store.load();
		const panel = createSettingsPanel(store);
		const engine = new SubtitleEngine(store.get());
		const shortcut = createShortcutController({
			getSettings: store.get,
			onRevealChange: (revealing) => engine.setRevealing(revealing)
		});
		let adapterHandle = null;
		const activeAdapter = adapter ?? detectSiteAdapter();
		if (activeAdapter) adapterHandle = activeAdapter.setup((snapshot) => engine.setSnapshot(snapshot));
		const unsubscribeSettings = store.subscribe((settings) => {
			engine.setSettings(settings);
			shortcut.setSettings(settings);
		});
		const { t } = createTranslator(store.get().language);
		registerMenuCommand?.(t("menuOpenSettings"), panel.open);
		return {
			openSettings: panel.open,
			destroy: () => {
				unsubscribeSettings();
				adapterHandle?.destroy();
				shortcut.destroy();
				engine.destroy();
				panel.destroy();
			}
		};
	}
	//#endregion
	//#region src/userscript/gm-storage.ts
	function createGmStorage(keyPrefix = "") {
		const keyFor = (key) => `${keyPrefix}${key}`;
		return {
			get: async (key, fallback) => {
				const storageKey = keyFor(key);
				if (typeof GM_getValue === "function") return Promise.resolve(GM_getValue(storageKey, fallback));
				const raw = localStorage.getItem(storageKey);
				if (raw === null) return fallback;
				try {
					return JSON.parse(raw);
				} catch {
					return fallback;
				}
			},
			set: async (key, value) => {
				const storageKey = keyFor(key);
				if (typeof GM_setValue === "function") {
					await Promise.resolve(GM_setValue(storageKey, value));
					return;
				}
				localStorage.setItem(storageKey, JSON.stringify(value));
			}
		};
	}
	//#endregion
	//#region src/userscript/menu.ts
	function registerUserscriptMenuCommand(label, callback) {
		if (typeof GM_registerMenuCommand === "function") GM_registerMenuCommand(label, callback);
	}
	//#endregion
	//#region src/main.ts
	bootstrapSubtitleShield({
		storage: createGmStorage(),
		registerMenuCommand: registerUserscriptMenuCommand
	}).then((app) => {});
	//#endregion
})();