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
})();