MES(Mobile Element Selector)

Material M3의 진보한 디자인, 아름다운 애니메이션, 완벽한 기능을 가진 모바일 요소 선택기

  1. // ==UserScript==
  2. // @name MES(Mobile Element Selector)
  3. // @author 삼플
  4. // @version 1.2.8
  5. // @description Material M3의 진보한 디자인, 아름다운 애니메이션, 완벽한 기능을 가진 모바일 요소 선택기
  6. // @match *://*/*
  7. // @license MIT
  8. // @grant GM_setClipboard
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @namespace https://adguard.com
  12. // ==/UserScript==
  13.  
  14. (async function() {
  15. 'use strict';
  16. const SCRIPT_ID = "[MES v1.2.8 M3]";
  17. const ADGUARD_LOGO_URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/AdGuard.svg/500px-AdGuard.svg.png';
  18.  
  19. const STRINGS = {
  20. panelTitle: '요소 차단',
  21. settingsTitle: 'MES by 삼플',
  22. listTitle: '저장된 차단 규칙',
  23. selectedElementLabel: '선택된 요소 (CSS 선택자)',
  24. parentLevelLabel: '상위 요소 선택 레벨:',
  25. copy: '복사',
  26. preview: '미리보기',
  27. restorePreview: '되돌리기',
  28. saveRule: '규칙 저장',
  29. list: '목록',
  30. settings: '설정',
  31. cancel: '취소',
  32. close: '닫기',
  33. includeSiteNameLabel: '규칙에 사이트명 포함',
  34. useAdguardLogoLabel: '토글 버튼 AdGuard 로고',
  35. panelOpacityLabel: '패널 투명도',
  36. toggleSizeLabel: '토글 버튼 크기',
  37. toggleOpacityLabel: '토글 버튼 투명도',
  38. tempDisableLabel: '모든 규칙 임시 비활성화',
  39. backupLabel: '규칙 백업 (JSON)',
  40. restoreLabel: '규칙 복원 (JSON)',
  41. togglePositionLabel: '토글 버튼 위치',
  42. posTopLeft: '좌상',
  43. posTopRight: '우상',
  44. posBottomLeft: '좌하',
  45. posBottomRight: '우하',
  46. on: 'ON',
  47. off: 'OFF',
  48. noRules: '저장된 규칙이 없습니다.',
  49. noElementSelected: '⚠️ 선택된 요소가 없습니다.',
  50. cannotGenerateSelector: '❌ 유효한 선택자를 생성할 수 없습니다.',
  51. selectorCopied: '✅ 선택자가 복사되었습니다!',
  52. clipboardError: '❌ 클립보드 복사 실패',
  53. promptCopy: '선택자를 직접 복사하세요:',
  54. alreadyHidden: 'ℹ️ 이미 숨겨진 요소입니다.',
  55. previewDifferentElement: '⚠️ 다른 요소가 미리보기 중입니다.',
  56. ruleSavedReloading: '✅ 규칙 저장됨! 적용 중...',
  57. ruleSavedApplyFailed: '⚠️ 규칙은 저장했으나 즉시 적용 실패.',
  58. ruleAddError: '❌ 규칙 추가 중 오류:',
  59. ruleExists: 'ℹ️ 이미 저장된 규칙입니다.',
  60. listShowError: '❌ 목록 표시 중 오류 발생',
  61. ruleCopied: '✅ 규칙 복사됨',
  62. ruleDeleted: '🗑️ 규칙 삭제됨',
  63. ruleDeleteError: '❌ 규칙 삭제 실패',
  64. settingsSaved: '✅ 설정 저장됨',
  65. settingsSaveError: '❌ 설정 저장 실패',
  66. backupStarting: '💾 규칙 백업 파일 다운로드를 시작합니다.',
  67. backupError: '❌ 규칙 백업 실패',
  68. restorePrompt: '📁 복원할 JSON 파일을 선택하세요.',
  69. restoreSuccess: '✅ 규칙 복원 완료! 적용 중...',
  70. restoreErrorInvalidFile: '❌ 잘못된 파일 형식 또는 내용입니다.',
  71. restoreErrorGeneral: '❌ 규칙 복원 실패',
  72. blockingApplied: (count) => `✅ ${count}개의 규칙 적용됨`,
  73. blockingApplyError: '❌ 규칙 적용 중 오류 발생',
  74. tempBlockingOn: '🚫 모든 규칙 임시 비활성화됨',
  75. tempBlockingOff: '✅ 규칙 다시 활성화됨'
  76. };
  77.  
  78. const DEFAULT_SETTINGS = {
  79. includeSiteName: true,
  80. panelOpacity: 0.65,
  81. toggleSizeScale: 1.0,
  82. toggleOpacity: 1.0,
  83. showAdguardLogo: false,
  84. tempBlockingDisabled: false,
  85. toggleBtnCorner: 'bottom-right'
  86. };
  87.  
  88. let settings = {};
  89. const SETTINGS_KEY = 'mobileElementSelectorSettings_v1_2';
  90. const BLOCKED_SELECTORS_KEY = 'mobileBlockedSelectors_v2';
  91.  
  92. async function loadSettings() {
  93. let storedSettings = {};
  94. try {
  95. const storedValue = await GM_getValue(SETTINGS_KEY, JSON.stringify(DEFAULT_SETTINGS));
  96. storedSettings = JSON.parse(storedValue || '{}');
  97. } catch (e) {
  98. console.error(SCRIPT_ID, `Error loading settings from GM_getValue('${SETTINGS_KEY}'), using defaults.`, e);
  99. storedSettings = {
  100. ...DEFAULT_SETTINGS
  101. };
  102. }
  103.  
  104. settings = {
  105. ...DEFAULT_SETTINGS,
  106. ...storedSettings
  107. };
  108.  
  109. settings.panelOpacity = parseFloat(settings.panelOpacity);
  110. if (isNaN(settings.panelOpacity) || settings.panelOpacity < 0.1 || settings.panelOpacity > 1.0) {
  111. settings.panelOpacity = DEFAULT_SETTINGS.panelOpacity;
  112. }
  113. settings.toggleSizeScale = parseFloat(settings.toggleSizeScale);
  114. if (isNaN(settings.toggleSizeScale) || settings.toggleSizeScale < 0.5 || settings.toggleSizeScale > 2.0) {
  115. settings.toggleSizeScale = DEFAULT_SETTINGS.toggleSizeScale;
  116. }
  117. settings.toggleOpacity = parseFloat(settings.toggleOpacity);
  118. if (isNaN(settings.toggleOpacity) || settings.toggleOpacity < 0.1 || settings.toggleOpacity > 1.0) {
  119. settings.toggleOpacity = DEFAULT_SETTINGS.toggleOpacity;
  120. }
  121. settings.includeSiteName = typeof settings.includeSiteName === 'boolean' ? settings.includeSiteName : DEFAULT_SETTINGS.includeSiteName;
  122. settings.showAdguardLogo = typeof settings.showAdguardLogo === 'boolean' ? settings.showAdguardLogo : DEFAULT_SETTINGS.showAdguardLogo;
  123. settings.tempBlockingDisabled = typeof settings.tempBlockingDisabled === 'boolean' ? settings.tempBlockingDisabled : DEFAULT_SETTINGS.tempBlockingDisabled;
  124.  
  125. const validCorners = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
  126. if (!validCorners.includes(settings.toggleBtnCorner)) {
  127. settings.toggleBtnCorner = DEFAULT_SETTINGS.toggleBtnCorner;
  128. }
  129.  
  130. console.log(SCRIPT_ID, "Settings loaded:", settings);
  131. }
  132.  
  133. async function saveSettings() {
  134. try {
  135. await GM_setValue(SETTINGS_KEY, JSON.stringify(settings));
  136. console.log(SCRIPT_ID, "Settings saved:", settings);
  137. } catch (e) {
  138. console.error(SCRIPT_ID, `Error saving settings to GM_setValue('${SETTINGS_KEY}')`, e);
  139. showToast(STRINGS.settingsSaveError, 'error');
  140. }
  141. }
  142.  
  143. const style = document.createElement('style');
  144.  
  145. function updateCSSVariables() {
  146. document.documentElement.style.setProperty('--panel-opacity', settings.panelOpacity);
  147. document.documentElement.style.setProperty('--toggle-size', `${56 * settings.toggleSizeScale}px`);
  148. document.documentElement.style.setProperty('--toggle-opacity', settings.toggleOpacity);
  149.  
  150. document.querySelectorAll('#mobile-block-panel, #mobile-settings-panel, #mobile-blocklist-panel').forEach(p => {
  151. p.style.setProperty('background-color', `rgba(40, 43, 48, ${settings.panelOpacity})`, 'important');
  152. });
  153. if (toggleBtn) {
  154. toggleBtn.style.setProperty('width', `var(--toggle-size)`, 'important');
  155. toggleBtn.style.setProperty('height', `var(--toggle-size)`, 'important');
  156. toggleBtn.style.setProperty('opacity', `var(--toggle-opacity)`, 'important');
  157. }
  158. }
  159.  
  160. function applyToggleBtnPosition() {
  161. if (!toggleBtn) return;
  162.  
  163. toggleBtn.style.top = 'auto';
  164. toggleBtn.style.left = 'auto';
  165. toggleBtn.style.bottom = 'auto';
  166. toggleBtn.style.right = 'auto';
  167. toggleBtn.style.transform = '';
  168.  
  169. const margin = '20px';
  170.  
  171. switch (settings.toggleBtnCorner) {
  172. case 'top-left':
  173. toggleBtn.style.top = margin;
  174. toggleBtn.style.left = margin;
  175. break;
  176. case 'top-right':
  177. toggleBtn.style.top = margin;
  178. toggleBtn.style.right = margin;
  179. break;
  180. case 'bottom-left':
  181. toggleBtn.style.bottom = margin;
  182. toggleBtn.style.left = margin;
  183. break;
  184. case 'bottom-right':
  185. default:
  186. toggleBtn.style.bottom = margin;
  187. toggleBtn.style.right = margin;
  188. break;
  189. }
  190. console.log(SCRIPT_ID, "Applied toggle button corner:", settings.toggleBtnCorner);
  191. }
  192.  
  193. style.textContent = `
  194. :root {
  195. --md-sys-color-primary: #a0c9ff; --md-sys-color-on-primary: #00325a;
  196. --md-sys-color-primary-container: #004880; --md-sys-color-on-primary-container: #d1e4ff;
  197. --md-sys-color-secondary: #bdc7dc; --md-sys-color-on-secondary: #283141;
  198. --md-sys-color-secondary-container: #3e4758; --md-sys-color-on-secondary-container: #dae2f9;
  199. --md-sys-color-tertiary: #e0bddd; --md-sys-color-on-tertiary: #402843;
  200. --md-sys-color-tertiary-container: #583e5a; --md-sys-color-on-tertiary-container: #fdd9fa;
  201. --md-sys-color-error: #ffb4ab; --md-sys-color-on-error: #690005;
  202. --md-sys-color-error-container: #93000a; --md-sys-color-on-error-container: #ffdad6;
  203. --md-sys-color-background: #1a1c1e; --md-sys-color-on-background: #e3e2e6;
  204. --md-sys-color-surface: #1a1c1e; --md-sys-color-on-surface: #e3e2e6;
  205. --md-sys-color-surface-variant: #43474e; --md-sys-color-on-surface-variant: #c3c6cf;
  206. --md-sys-color-outline: #8d9199; --md-sys-color-shadow: #000000;
  207. --md-sys-color-inverse-surface: #e3e2e6; --md-sys-color-inverse-on-surface: #2f3033;
  208. --md-sys-color-surface-container-high: rgba(227, 226, 230, 0.16);
  209. --md-sys-color-success: #90ee90; --md-sys-color-success-container: rgba(144, 238, 144, 0.1);
  210. --md-sys-color-warning: #ffcc80;
  211. --panel-opacity: ${DEFAULT_SETTINGS.panelOpacity};
  212. --toggle-size: ${56 * DEFAULT_SETTINGS.toggleSizeScale}px;
  213. --toggle-opacity: ${DEFAULT_SETTINGS.toggleOpacity};
  214. --md-ref-typeface-plain: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  215. --md-sys-typescale-body-large-font-family: var(--md-ref-typeface-plain);
  216. --md-sys-typescale-body-large-font-size: 16px;
  217. --md-sys-typescale-label-large-font-size: 14px;
  218. --md-sys-typescale-label-medium-font-size: 12px;
  219. --md-sys-typescale-label-small-font-size: 11px;
  220. --md-sys-typescale-title-medium-font-size: 18px;
  221. }
  222.  
  223. .scrollable-container {
  224. position: relative;
  225. overflow-y: auto;
  226. max-height: 70vh;
  227. padding: 20px;
  228. -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
  229. mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
  230. scrollbar-width: none;
  231. -ms-overflow-style: none;
  232. }
  233. .scrollable-container::-webkit-scrollbar {
  234. display: none;
  235. }
  236.  
  237. .mobile-block-ui { z-index: 2147483646 !important; touch-action: manipulation !important; font-family: var(--md-sys-typescale-body-large-font-family); box-sizing: border-box; position: fixed !important; visibility: visible !important; color: var(--md-sys-color-on-surface); -webkit-tap-highlight-color: transparent !important; }
  238.  
  239. #mobile-block-panel, #mobile-settings-panel, #mobile-blocklist-panel {
  240. background-color: rgba(40, 43, 48, var(--panel-opacity)) !important; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  241. color: var(--md-sys-color-on-surface); border-radius: 20px !important;
  242. box-shadow: 0 12px 17px 2px rgba(0,0,0,0.14), 0 5px 22px 4px rgba(0,0,0,0.12), 0 7px 8px -4px rgba(0,0,0,0.20) !important;
  243. border: 1px solid rgba(255, 255, 255, 0.12); padding: 18px 20px; width: calc(100% - 40px); max-width: 380px;
  244. display: none;
  245. opacity: 0;
  246. backface-visibility: hidden; -webkit-backface-visibility: hidden; overflow: hidden;
  247. transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-out;
  248. will-change: transform, opacity;
  249. cursor: grab;
  250. }
  251.  
  252. #mobile-block-panel { bottom: 20px; left: 50%; transform: translateX(-50%) translateY(100px) scale(0.95); z-index: 2147483645 !important; }
  253. #mobile-settings-panel, #mobile-blocklist-panel { top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); z-index: 2147483647 !important; max-width: 340px; max-height: 90vh;overflow-y: auto;}
  254.  
  255. #mobile-block-panel.visible {
  256. opacity: 1;
  257. transform: translateX(-50%) translateY(0) scale(1);
  258. }
  259. #mobile-settings-panel.visible, #mobile-blocklist-panel.visible {
  260. opacity: 1;
  261. transform: translate(-50%, -50%) scale(1);
  262. }
  263.  
  264. /* Overrides for dragged panels: transform should only handle scale */
  265. #mobile-block-panel[data-was-dragged="true"] {
  266. transform: scale(0.95); /* Closed state */
  267. }
  268. #mobile-block-panel[data-was-dragged="true"].visible {
  269. transform: scale(1); /* Open state */
  270. }
  271. #mobile-settings-panel[data-was-dragged="true"],
  272. #mobile-blocklist-panel[data-was-dragged="true"] {
  273. transform: scale(0.9); /* Closed state */
  274. }
  275. #mobile-settings-panel[data-was-dragged="true"].visible,
  276. #mobile-blocklist-panel[data-was-dragged="true"].visible {
  277. transform: scale(1); /* Open state */
  278. }
  279.  
  280. .mb-panel-title { font-size: var(--md-sys-typescale-title-medium-font-size); font-weight: 500; color: var(--md-sys-color-on-surface); text-align: center; margin: 0 0 24px 0; }
  281.  
  282. .mb-slider { width: 100%; margin: 15px 0; -webkit-appearance: none; appearance: none; background: var(--md-sys-color-surface-variant); height: 5px; border-radius: 3px; outline: none; cursor: pointer; transition: background 0.3s ease; }
  283. .mb-slider:hover { background: var(--md-sys-color-outline); }
  284. .mb-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: var(--md-sys-color-primary); border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 1px 3px rgba(0,0,0,0.4); transition: background 0.3s ease, box-shadow 0.3s ease; }
  285. .mb-slider::-moz-range-thumb { width: 22px; height: 22px; background: var(--md-sys-color-primary); border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 1px 3px rgba(0,0,0,0.4); transition: background 0.3s ease, box-shadow 0.3s ease; }
  286. .mb-slider:active::-webkit-slider-thumb { box-shadow: 0 0 0 10px rgba(var(--md-sys-color-primary-rgb, 160, 201, 255), 0.25); }
  287. .mb-slider:active::-moz-range-thumb { box-shadow: 0 0 0 10px rgba(var(--md-sys-color-primary-rgb, 160, 201, 255), 0.25); }
  288.  
  289. .selected-element {
  290. outline: 3px solid var(--md-sys-color-error) !important;
  291. outline-offset: 2px;
  292. box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45) !important;
  293. background-color: rgba(255, 82, 82, 0.15) !important;
  294. z-index: 2147483644 !important;
  295. transition: background-color 0.1s ease, outline 0.1s ease, box-shadow 0.1s ease;
  296. pointer-events: none;
  297. }
  298.  
  299. #mobile-block-toggleBtn {
  300. z-index: 2147483646 !important; background-color: var(--md-sys-color-primary-container) !important; color: var(--md-sys-color-on-primary-container) !important;
  301. opacity: var(--toggle-opacity) !important; width: var(--toggle-size) !important; height: var(--toggle-size) !important; border-radius: 18px !important; border: none !important; cursor: pointer !important;
  302. box-shadow: 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12), 0 3px 5px -1px rgba(0,0,0,0.20) !important;
  303. transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease, opacity 0.3s ease, border 0.2s ease, top 0.3s ease, left 0.3s ease, bottom 0.3s ease, right 0.3s ease;
  304. display: flex !important; align-items: center !important; justify-content: center !important; overflow: hidden !important; backface-visibility: hidden; -webkit-backface-visibility: hidden; position: fixed !important; -webkit-tap-highlight-color: transparent !important;
  305. }
  306. #mobile-block-toggleBtn:active { transform: scale(0.95); box-shadow: 0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12) !important; }
  307. #mobile-block-toggleBtn.selecting {
  308. background-color: var(--md-sys-color-primary) !important;
  309. color: var(--md-sys-color-on-primary) !important;
  310. box-shadow: 0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12), 0 5px 5px -3px rgba(0,0,0,0.20) !important;
  311. }
  312. #mobile-block-toggleBtn .toggle-icon { width: 55%; height: 55%; display: block; margin: auto; background-color: currentColor; mask-size: contain; mask-repeat: no-repeat; mask-position: center; -webkit-mask-size: contain; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; }
  313. #mobile-block-toggleBtn .toggle-icon-plus { mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>'); -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>'); }
  314. #mobile-block-toggleBtn.selecting .toggle-icon-plus { mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>'); -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>'); }
  315. #mobile-block-toggleBtn .toggle-icon-adguard { background-image: url('${ADGUARD_LOGO_URL}'); background-size: contain; background-repeat: no-repeat; background-position: center; background-color: transparent !important; mask-image: none; -webkit-mask-image: none; width: 60%; height: 60%; }
  316.  
  317. .mb-btn { padding: 10px 24px; border: none; border-radius: 20px !important; font-size: var(--md-sys-typescale-label-large-font-size); font-weight: 500; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease; text-align: center; box-shadow: 0 1px 2px 0 rgba(0,0,0,0.3), 0 1px 3px 1px rgba(0,0,0,0.15); min-width: 64px; min-height: 40px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; opacity: 1 !important; -webkit-tap-highlight-color: transparent !important; line-height: 1.5; display: inline-flex; align-items: center; justify-content: center; }
  318. .mb-btn:hover { box-shadow: 0 1px 2px 0 rgba(0,0,0,0.3), 0 2px 6px 2px rgba(0,0,0,0.15); }
  319. .mb-btn:active { transform: scale(0.97); box-shadow: none; }
  320. .mb-btn.primary { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
  321. .mb-btn.primary:hover { background-color: #b0d3ff; } .mb-btn.primary:active { background-color: #c0daff; }
  322. .mb-btn.secondary { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
  323. .mb-btn.secondary:hover { background-color: #545d6e; } .mb-btn.secondary:active { background-color: #6a7385; }
  324. .mb-btn.tertiary { background-color: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container); }
  325. .mb-btn.tertiary:hover { background-color: #6f5471; } .mb-btn.tertiary:active { background-color: #866a89; }
  326. .mb-btn.error { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }
  327. .mb-btn.error:hover { background-color: #b12025; } .mb-btn.error:active { background-color: #c83c40; }
  328. .mb-btn.surface { background-color: var(--md-sys-color-surface-variant); color: var(--md-sys-color-on-surface-variant); }
  329. .mb-btn.surface:hover { background-color: #53575e; } .mb-btn.surface:active { background-color: #63676e; }
  330. .mb-btn.outline { background-color: transparent; color: var(--md-sys-color-primary); border: 1px solid var(--md-sys-color-outline); box-shadow: none; }
  331. .mb-btn.outline:hover { background-color: rgba(var(--md-sys-color-primary-rgb, 160, 201, 255), 0.08); }
  332. .mb-btn.outline:active { background-color: rgba(var(--md-sys-color-primary-rgb, 160, 201, 255), 0.12); }
  333.  
  334. .button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 12px; margin-top: 24px; }
  335. #blocker-info-wrapper { margin-bottom: 15px; padding: 10px 14px; background-color: var(--md-sys-color-surface-variant); border-radius: 12px; border: 1px solid var(--md-sys-color-outline); }
  336. #blocker-info-label { display: block; font-size: var(--md-sys-typescale-label-medium-font-size); color: var(--md-sys-color-on-surface-variant); margin-bottom: 6px; font-weight: 500; }
  337. #blocker-info { display: block; color: var(--md-sys-color-on-surface); font-size: var(--md-sys-typescale-label-large-font-size); line-height: 1.45; word-break: break-all; min-height: 1.45em; font-family: 'Consolas', 'Monaco', monospace; max-height: 6em; overflow-y: auto; }
  338. #blocker-info:empty::after { content: '없음'; color: var(--md-sys-color-on-surface-variant); font-style: italic; }
  339. label[for="blocker-slider"] { display: block; font-size: var(--md-sys-typescale-label-medium-font-size); color: var(--md-sys-color-on-surface-variant); margin-bottom: 5px; margin-top: 10px; }
  340.  
  341. .settings-item { margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px; }
  342. .settings-item label { display: flex; justify-content: space-between; align-items: center; font-size: var(--md-sys-typescale-label-large-font-size); color: var(--md-sys-color-on-surface-variant); }
  343. .settings-item label .settings-label-text { flex-grow: 1; margin-right: 10px; }
  344. .settings-value { color: var(--md-sys-color-on-surface); font-weight: 500; font-size: var(--md-sys-typescale-label-medium-font-size); padding-left: 10px; }
  345. #settings-toggle-site, #settings-adguard-logo, #settings-temp-disable { min-width: 70px; padding: 8px 14px; font-size: var(--md-sys-typescale-label-medium-font-size); flex-shrink: 0; }
  346. #settings-toggle-site.active, #settings-adguard-logo.active, #settings-temp-disable.active { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
  347. #settings-toggle-site:not(.active), #settings-adguard-logo:not(.active), #settings-temp-disable:not(.active) { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
  348. #settings-close, #settings-backup, #settings-restore { width: 100%; margin-top: 10px; }
  349. #settings-restore-input { display: none; }
  350.  
  351. .corner-selector-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-top: 5px; }
  352. .corner-btn { padding: 8px 12px; min-width: 60px; font-size: var(--md-sys-typescale-label-medium-font-size); }
  353. .corner-btn.active { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
  354. .corner-btn:not(.active) { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
  355.  
  356. #blocklist-container { max-height: calc(70vh - 150px); overflow-y: auto; margin: 20px 0; padding-right: 8px; display: flex; flex-direction: column; gap: 10px; }
  357. .blocklist-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; background-color: rgba(var(--md-sys-color-surface-variant-rgb, 67, 71, 78), 0.5); border-radius: 12px; border: 1px solid transparent; transition: background-color 0.2s, border-color 0.2s, opacity 0.3s ease, transform 0.3s ease; }
  358. .blocklist-item:hover { background-color: rgba(var(--md-sys-color-surface-variant-rgb, 67, 71, 78), 0.7); border-color: var(--md-sys-color-outline); }
  359. .blocklist-item span { flex: 1; word-break: break-all; margin-right: 12px; font-size: var(--md-sys-typescale-label-medium-font-size); color: var(--md-sys-color-on-surface-variant); font-family: 'Consolas', 'Monaco', monospace; }
  360. .blocklist-controls { display: flex; gap: 6px; flex-shrink: 0; }
  361. .blocklist-btn { padding: 6px 10px; min-width: auto; min-height: 32px; font-size: var(--md-sys-typescale-label-small-font-size); border-radius: 16px !important; }
  362. .blocklist-btn-delete { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }
  363. .blocklist-btn-copy { background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
  364. #blocklist-empty { text-align:center; color: var(--md-sys-color-on-surface-variant); padding: 20px 0; }
  365.  
  366. #mes-toast-container { position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%); z-index: 2147483647 !important; display: flex; flex-direction: column-reverse; align-items: center; gap: 10px; pointer-events: none; width: max-content; max-width: 90%; }
  367. .mes-toast { background-color: var(--md-sys-color-inverse-surface); color: var(--md-sys-color-inverse-on-surface); padding: 14px 20px; border-radius: 8px; box-shadow: 0 3px 5px -1px rgba(0,0,0,0.2), 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12); font-size: var(--md-sys-typescale-label-large-font-size); opacity: 0; transform: translateY(20px); transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease; pointer-events: all; max-width: 100%; text-align: center; }
  368. .mes-toast.show { opacity: 1; transform: translateY(0); }
  369. .mes-toast.info { background-color: #333; color: white; }
  370. .mes-toast.success { background-color: var(--md-sys-color-success-container); color: var(--md-sys-color-success); }
  371. .mes-toast.error { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }
  372. .mes-toast.warning { background-color: #4d3a00; color: var(--md-sys-color-warning); }
  373. `;
  374. document.head.appendChild(style);
  375.  
  376. let panel, settingsPanel, toggleBtn, listPanel, toastContainer;
  377.  
  378. function createUIElements() {
  379. toastContainer = document.createElement('div');
  380. toastContainer.id = 'mes-toast-container';
  381. toastContainer.className = 'mobile-block-ui';
  382. document.body.appendChild(toastContainer);
  383.  
  384. panel = document.createElement('div');
  385. panel.id = 'mobile-block-panel';
  386. panel.className = 'mobile-block-ui';
  387. panel.innerHTML = `
  388. <div id="blocker-info-wrapper">
  389. <span id="blocker-info-label">${STRINGS.selectedElementLabel}</span>
  390. <div id="blocker-info"></div>
  391. </div>
  392. <label for="blocker-slider" style="display: block; font-size: var(--md-sys-typescale-label-medium-font-size); color: var(--md-sys-color-on-surface-variant); margin-bottom: 5px;">${STRINGS.parentLevelLabel}</label>
  393. <input type="range" id="blocker-slider" class="mb-slider" min="0" max="10" value="0" aria-label="Parent Level Selector">
  394. <div class="button-grid">
  395. <button id="blocker-copy" class="mb-btn secondary">${STRINGS.copy}</button>
  396. <button id="blocker-preview" class="mb-btn secondary">${STRINGS.preview}</button>
  397. <button id="blocker-add-block" class="mb-btn primary">${STRINGS.saveRule}</button>
  398. <button id="blocker-list" class="mb-btn tertiary">${STRINGS.list}</button>
  399. <button id="blocker-settings" class="mb-btn tertiary">${STRINGS.settings}</button>
  400. <button id="blocker-cancel" class="mb-btn surface">${STRINGS.cancel}</button>
  401. </div>`;
  402. document.body.appendChild(panel);
  403.  
  404. listPanel = document.createElement('div');
  405. listPanel.id = 'mobile-blocklist-panel';
  406. listPanel.className = 'mobile-block-ui';
  407. listPanel.innerHTML = `
  408. <h3 class="mb-panel-title">${STRINGS.listTitle}</h3>
  409. <div id="blocklist-container"></div>
  410. <button id="blocklist-close" class="mb-btn surface" style="width: 100%; margin-top: 15px;">${STRINGS.close}</button>`;
  411. document.body.appendChild(listPanel);
  412.  
  413. settingsPanel = document.createElement('div');
  414. settingsPanel.id = 'mobile-settings-panel';
  415. settingsPanel.className = 'mobile-block-ui';
  416. settingsPanel.innerHTML = `
  417. <h3 class="mb-panel-title">${STRINGS.settingsTitle}</h3>
  418. <div class="scrollable-container" style="max-height: 65vh; overflow-y: auto;">
  419. <div class="settings-item">
  420. <label><span class="settings-label-text">${STRINGS.includeSiteNameLabel}</span>
  421. <button id="settings-toggle-site" class="mb-btn ${settings.includeSiteName ? 'active' : ''}">${settings.includeSiteName ? STRINGS.on : STRINGS.off}</button>
  422. </label>
  423. </div>
  424. <div class="settings-item">
  425. <label><span class="settings-label-text">${STRINGS.useAdguardLogoLabel}</span>
  426. <button id="settings-adguard-logo" class="mb-btn ${settings.showAdguardLogo ? 'active' : ''}">${settings.showAdguardLogo ? STRINGS.on : STRINGS.off}</button>
  427. </label>
  428. </div>
  429. <div class="settings-item">
  430. <label><span class="settings-label-text">${STRINGS.tempDisableLabel}</span>
  431. <button id="settings-temp-disable" class="mb-btn ${settings.tempBlockingDisabled ? 'active error' : 'secondary'}">${settings.tempBlockingDisabled ? STRINGS.on : STRINGS.off}</button>
  432. </label>
  433. </div>
  434. <div class="settings-item">
  435. <label for="settings-panel-opacity">
  436. <span class="settings-label-text">${STRINGS.panelOpacityLabel}</span>
  437. <span id="opacity-value" class="settings-value">${settings.panelOpacity.toFixed(2)}</span>
  438. </label>
  439. <input id="settings-panel-opacity" type="range" class="mb-slider" min="0.1" max="1.0" step="0.05" value="${settings.panelOpacity}" aria-label="Panel Opacity">
  440. </div>
  441. <div class="settings-item">
  442. <label for="settings-toggle-size">
  443. <span class="settings-label-text">${STRINGS.toggleSizeLabel}</span>
  444. <span id="toggle-size-value" class="settings-value">${settings.toggleSizeScale.toFixed(1)}x</span>
  445. </label>
  446. <input id="settings-toggle-size" type="range" class="mb-slider" min="0.5" max="2.0" step="0.1" value="${settings.toggleSizeScale}" aria-label="Toggle Button Size">
  447. </div>
  448. <div class="settings-item">
  449. <label for="settings-toggle-opacity">
  450. <span class="settings-label-text">${STRINGS.toggleOpacityLabel}</span>
  451. <span id="toggle-opacity-value" class="settings-value">${settings.toggleOpacity.toFixed(2)}</span>
  452. </label>
  453. <input id="settings-toggle-opacity" type="range" class="mb-slider" min="0.1" max="1.0" step="0.05" value="${settings.toggleOpacity}" aria-label="Toggle Button Opacity">
  454. </div>
  455. <div class="settings-item">
  456. <label><span class="settings-label-text">${STRINGS.togglePositionLabel}</span></label>
  457. <div class="corner-selector-grid">
  458. <button id="corner-tl" data-corner="top-left" class="mb-btn corner-btn">${STRINGS.posTopLeft}</button>
  459. <button id="corner-tr" data-corner="top-right" class="mb-btn corner-btn">${STRINGS.posTopRight}</button>
  460. <button id="corner-bl" data-corner="bottom-left" class="mb-btn corner-btn">${STRINGS.posBottomLeft}</button>
  461. <button id="corner-br" data-corner="bottom-right" class="mb-btn corner-btn">${STRINGS.posBottomRight}</button>
  462. </div>
  463. </div>
  464. <div class="button-grid" style="margin-top: 20px; grid-template-columns: 1fr 1fr;">
  465. <button id="settings-backup" class="mb-btn outline">${STRINGS.backupLabel}</button>
  466. <button id="settings-restore" class="mb-btn outline">${STRINGS.restoreLabel}</button>
  467. <input type="file" id="settings-restore-input" accept=".json">
  468. </div>
  469. </div>
  470. <button id="settings-close" class="mb-btn surface" style="width: 100%; margin-top: 20px;">${STRINGS.close}</button>`;
  471. document.body.appendChild(settingsPanel);
  472.  
  473. toggleBtn = document.createElement('button');
  474. toggleBtn.id = 'mobile-block-toggleBtn';
  475. toggleBtn.className = 'mobile-block-ui';
  476. toggleBtn.setAttribute('aria-label', 'Toggle Element Selector');
  477. document.body.appendChild(toggleBtn);
  478.  
  479. updateCSSVariables();
  480. updateToggleIcon();
  481. applyToggleBtnPosition();
  482.  
  483. initRefsAndEvents();
  484. applyBlocking();
  485. }
  486.  
  487. function showToast(message, type = 'info', duration = 3000) {
  488. if (!toastContainer) {
  489. console.warn(SCRIPT_ID, "Toast container not ready for message:", message);
  490. return;
  491. }
  492. const toast = document.createElement('div');
  493. toast.className = `mes-toast ${type}`;
  494. toast.textContent = message;
  495. toastContainer.appendChild(toast);
  496.  
  497. void toast.offsetWidth;
  498.  
  499. requestAnimationFrame(() => {
  500. toast.classList.add('show');
  501. });
  502.  
  503. setTimeout(() => {
  504. toast.classList.remove('show');
  505. toast.addEventListener('transitionend', () => {
  506. try {
  507. toast.remove();
  508. } catch (e) {}
  509. }, {
  510. once: true
  511. });
  512. setTimeout(() => {
  513. try {
  514. toast.remove();
  515. } catch (e) {}
  516. }, 500);
  517. }, duration);
  518. }
  519.  
  520. let selecting = false;
  521. let selectedEl = null;
  522. let initialTouchedElement = null;
  523. let touchStartX = 0,
  524. touchStartY = 0,
  525. touchMoved = false;
  526. const moveThreshold = 15;
  527. let blockedSelectorsCache = [];
  528.  
  529. async function loadBlockedSelectors() {
  530. let stored = '[]';
  531. try {
  532. stored = await GM_getValue(BLOCKED_SELECTORS_KEY, '[]');
  533. const parsed = JSON.parse(stored);
  534. blockedSelectorsCache = Array.isArray(parsed) ? parsed : [];
  535. console.log(SCRIPT_ID, `Loaded ${blockedSelectorsCache.length} rules from storage.`);
  536. return blockedSelectorsCache;
  537. } catch (e) {
  538. console.error(SCRIPT_ID, `Error parsing blocked selectors from key '${BLOCKED_SELECTORS_KEY}', resetting. Stored value:`, stored, e);
  539. try {
  540. await GM_setValue(BLOCKED_SELECTORS_KEY, '[]');
  541. } catch (resetError) {
  542. console.error(SCRIPT_ID, "Failed to reset storage after parse error", resetError);
  543. }
  544. blockedSelectorsCache = [];
  545. return [];
  546. }
  547. }
  548.  
  549. async function saveBlockedSelectors(list) {
  550. const selectorsToSave = Array.isArray(list) ? list : [];
  551. try {
  552. await GM_setValue(BLOCKED_SELECTORS_KEY, JSON.stringify(selectorsToSave));
  553. blockedSelectorsCache = [...selectorsToSave];
  554. console.log(SCRIPT_ID, `Saved ${selectorsToSave.length} rules.`);
  555. } catch (e) {
  556. console.error(SCRIPT_ID, "Error saving blocked selectors to GM:", e);
  557. showToast(STRINGS.settingsSaveError, 'error');
  558. }
  559. }
  560.  
  561. const originalDisplayMap = new Map();
  562.  
  563. async function applyBlocking(showToastNotification = false) {
  564. if (settings.tempBlockingDisabled) {
  565. console.log(SCRIPT_ID, "Blocking temporarily disabled. Skipping application.");
  566. disableAllBlocking(false);
  567. return 0;
  568. }
  569.  
  570. console.log(SCRIPT_ID, "Applying block rules...");
  571. if (blockedSelectorsCache.length === 0) {
  572. await loadBlockedSelectors();
  573. }
  574.  
  575. let count = 0;
  576. let appliedCount = 0;
  577. const currentHostname = location.hostname;
  578.  
  579. blockedSelectorsCache.forEach(rule => {
  580. if (typeof rule !== 'string' || !rule.includes('##')) {
  581. console.warn(SCRIPT_ID, "Skipping invalid block rule format:", rule);
  582. return;
  583. }
  584.  
  585. const parts = rule.split('##');
  586. const domain = parts[0];
  587. const cssSelector = parts[1];
  588.  
  589. if (!cssSelector) {
  590. console.warn(SCRIPT_ID, "Skipping rule with empty selector:", rule);
  591. return;
  592. }
  593. if (domain && domain !== '*' && currentHostname !== domain) {
  594. return;
  595. }
  596.  
  597. try {
  598. const elements = document.querySelectorAll(cssSelector);
  599. elements.forEach(el => {
  600. const isHiddenByScript = el.style.display === 'none' && el.hasAttribute('data-mes-hidden');
  601. const isNaturallyHidden = window.getComputedStyle(el).display === 'none';
  602.  
  603. if (!isHiddenByScript && !isNaturallyHidden) {
  604. if (!originalDisplayMap.has(el)) {
  605. originalDisplayMap.set(el, el.style.display || 'unset');
  606. }
  607. el.style.setProperty('display', 'none', 'important');
  608. el.setAttribute('data-mes-hidden', 'true');
  609. count++;
  610. } else if (isHiddenByScript) {
  611. if (!originalDisplayMap.has(el)) {
  612. originalDisplayMap.set(el, 'unset');
  613. }
  614. }
  615. });
  616. if (elements.length > 0) appliedCount++;
  617.  
  618. } catch (e) {}
  619. });
  620.  
  621. if (count > 0) console.log(SCRIPT_ID, `Applied ${appliedCount} rules, hid ${count} new elements.`);
  622. else console.log(SCRIPT_ID, `Applied ${appliedCount} rules, no new elements needed hiding.`);
  623.  
  624. if (showToastNotification && appliedCount > 0 && !settings.tempBlockingDisabled) {
  625. showToast(STRINGS.blockingApplied(appliedCount), 'success', 2000);
  626. }
  627. return appliedCount;
  628. }
  629.  
  630. function disableAllBlocking(showToastNotification = true) {
  631. console.log(SCRIPT_ID, "Disabling all blocking rules temporarily...");
  632. let restoredCount = 0;
  633. document.querySelectorAll('[data-mes-hidden="true"]').forEach(el => {
  634. const originalDisplay = originalDisplayMap.get(el);
  635. if (originalDisplay === 'unset') {
  636. el.style.removeProperty('display');
  637. } else if (originalDisplay !== undefined) {
  638. el.style.setProperty('display', originalDisplay, '');
  639. } else {
  640. el.style.removeProperty('display');
  641. }
  642. el.removeAttribute('data-mes-hidden');
  643. restoredCount++;
  644. });
  645. console.log(SCRIPT_ID, `Restored display for ${restoredCount} elements.`);
  646. if (showToastNotification) {
  647. showToast(STRINGS.tempBlockingOn, 'warning', 2500);
  648. }
  649. }
  650.  
  651. async function enableAllBlocking(showToastNotification = true) {
  652. console.log(SCRIPT_ID, "Re-enabling blocking rules...");
  653. const appliedCount = await applyBlocking(false);
  654. if (showToastNotification && appliedCount > 0) {
  655. showToast(STRINGS.tempBlockingOff, 'success', 2000);
  656. } else if (showToastNotification) {
  657. showToast(STRINGS.tempBlockingOff, 'info', 1500);
  658. }
  659. }
  660.  
  661. function updateToggleIcon() {
  662. if (!toggleBtn) return;
  663. if (settings.showAdguardLogo) {
  664. toggleBtn.innerHTML = `<span class="toggle-icon toggle-icon-adguard" aria-hidden="true"></span>`;
  665. } else {
  666. toggleBtn.innerHTML = `<span class="toggle-icon toggle-icon-plus" aria-hidden="true"></span>`;
  667. }
  668. toggleBtn.classList.toggle('selecting', selecting);
  669. }
  670.  
  671. function generateSelector(el, maxDepth = 7, requireUnique = true) {
  672. if (!el || el.nodeType !== 1 || el.closest('.mobile-block-ui')) return '';
  673.  
  674. if (el.id) {
  675. const id = el.id;
  676. const escapedId = CSS.escape(id);
  677. if (!/^\d+$/.test(id) && id.length > 2 && !id.startsWith('ember') && !id.startsWith('react') && !id.includes(':')) {
  678. try {
  679. if (document.querySelectorAll(`#${escapedId}`).length === 1) {
  680. return `#${escapedId}`;
  681. }
  682. } catch (e) {}
  683. }
  684. }
  685.  
  686. const parts = [];
  687. let current = el;
  688. let depth = 0;
  689.  
  690. while (current && current.tagName && depth < maxDepth) {
  691. const tagName = current.tagName.toLowerCase();
  692. if (tagName === 'body' || tagName === 'html') break;
  693. if (current.closest('.mobile-block-ui')) {
  694. current = current.parentElement;
  695. continue;
  696. }
  697.  
  698. let part = tagName;
  699. let addedSpecificity = false;
  700.  
  701. const stableClasses = Array.from(current.classList)
  702. .filter(c => c && c.length > 2 &&
  703. !/^[a-z]{1,2}$/i.test(c) &&
  704. !/\d/.test(c) &&
  705. !/active|select|focus|hover|disabled|open|closed|visible|hidden|js-|ui-/i.test(c) &&
  706. !/^[A-Z0-9]{4,}$/.test(c) &&
  707. !c.includes('--') && !c.includes('__') &&
  708. !['selected-element', 'mobile-block-ui'].some(uiClass => c.includes(uiClass)))
  709. .slice(0, 2);
  710.  
  711. if (stableClasses.length > 0) {
  712. part += '.' + stableClasses.map(c => CSS.escape(c)).join('.');
  713. addedSpecificity = true;
  714. }
  715.  
  716. if (!addedSpecificity || (current.parentElement && !current.parentElement.closest('.mobile-block-ui'))) {
  717. const siblings = current.parentElement ? Array.from(current.parentElement.children) : [];
  718. const sameTagSiblings = siblings.filter(sib => sib.tagName === current.tagName && !sib.closest('.mobile-block-ui'));
  719.  
  720. if (sameTagSiblings.length > 1) {
  721. const index = sameTagSiblings.indexOf(current) + 1;
  722. if (index > 0) {
  723. part += `:nth-of-type(${index})`;
  724. addedSpecificity = true;
  725. }
  726. }
  727. }
  728.  
  729. parts.unshift(part);
  730.  
  731. if (requireUnique && parts.length > 0 && depth > 0) {
  732. const tempSelector = parts.join(' > ');
  733. try {
  734. if (document.querySelectorAll(tempSelector).length === 1) {
  735. console.log(SCRIPT_ID, `Unique selector found early: ${tempSelector}`);
  736. return tempSelector;
  737. }
  738. } catch (e) {}
  739. }
  740.  
  741. current = current.parentElement;
  742. depth++;
  743. }
  744.  
  745. let finalSelector = parts.join(' > ');
  746.  
  747. if (requireUnique && finalSelector) {
  748. try {
  749. const matches = document.querySelectorAll(finalSelector);
  750. if (matches.length !== 1) {
  751. console.warn(SCRIPT_ID, `Generated selector "${finalSelector}" matches ${matches.length} elements. Trying parent recursively.`);
  752. if (el.parentElement && !el.parentElement.closest('.mobile-block-ui') && maxDepth > 0) {
  753. const parentSelector = generateSelector(el.parentElement, maxDepth - 1, false);
  754. if (parentSelector) {
  755. const combinedSelector = parentSelector + " > " + finalSelector;
  756. try {
  757. if (document.querySelectorAll(combinedSelector).length === 1) {
  758. console.log(SCRIPT_ID, `Using combined unique selector: ${combinedSelector}`);
  759. return combinedSelector;
  760. } else {
  761. console.warn(SCRIPT_ID, `Combined selector "${combinedSelector}" still not unique.`);
  762. }
  763. } catch (e) {}
  764. }
  765. }
  766. console.warn(SCRIPT_ID, `Could not guarantee uniqueness for: ${finalSelector}`);
  767. return finalSelector;
  768. }
  769. } catch (e) {
  770. console.error(SCRIPT_ID, `Error validating selector "${finalSelector}":`, e);
  771. return '';
  772. }
  773. }
  774.  
  775. if (!finalSelector || finalSelector === 'body' || finalSelector === 'html') {
  776. return '';
  777. }
  778.  
  779. return finalSelector;
  780. }
  781.  
  782. function initRefsAndEvents() {
  783. const infoLabel = panel.querySelector('#blocker-info-label');
  784. const info = panel.querySelector('#blocker-info');
  785. const slider = panel.querySelector('#blocker-slider');
  786. const copyBtn = panel.querySelector('#blocker-copy');
  787. const previewBtn = panel.querySelector('#blocker-preview');
  788. const addBtn = panel.querySelector('#blocker-add-block');
  789. const listBtn = panel.querySelector('#blocker-list');
  790. const settingsBtn = panel.querySelector('#blocker-settings');
  791. const cancelBtn = panel.querySelector('#blocker-cancel');
  792.  
  793. const listContainer = listPanel.querySelector('#blocklist-container');
  794. const listClose = listPanel.querySelector('#blocklist-close');
  795.  
  796. const settingsClose = settingsPanel.querySelector('#settings-close');
  797. const toggleSiteBtn = settingsPanel.querySelector('#settings-toggle-site');
  798. const adguardLogoToggleBtn = settingsPanel.querySelector('#settings-adguard-logo');
  799. const tempDisableBtn = settingsPanel.querySelector('#settings-temp-disable');
  800. const panelOpacitySlider = settingsPanel.querySelector('#settings-panel-opacity');
  801. const panelOpacityValue = settingsPanel.querySelector('#opacity-value');
  802. const toggleSizeSlider = settingsPanel.querySelector('#settings-toggle-size');
  803. const toggleSizeValue = settingsPanel.querySelector('#toggle-size-value');
  804. const toggleOpacitySlider = settingsPanel.querySelector('#settings-toggle-opacity');
  805. const toggleOpacityValue = settingsPanel.querySelector('#toggle-opacity-value');
  806. const cornerButtons = settingsPanel.querySelectorAll('.corner-btn');
  807. const backupBtn = settingsPanel.querySelector('#settings-backup');
  808. const restoreBtn = settingsPanel.querySelector('#settings-restore');
  809. const restoreInput = settingsPanel.querySelector('#settings-restore-input');
  810.  
  811. let isPreviewHidden = false;
  812. let previewedElement = null;
  813.  
  814. function removeSelectionHighlight() {
  815. if (selectedEl) {
  816. selectedEl.classList.remove('selected-element');
  817. }
  818. selectedEl = null;
  819. if (slider) slider.value = 0;
  820. if (info) info.textContent = '';
  821. }
  822.  
  823. function resetPreview() {
  824. if (isPreviewHidden && previewedElement) {
  825. try {
  826. const originalDisplay = previewedElement.dataset._original_display;
  827. if (originalDisplay === 'unset') {
  828. previewedElement.style.removeProperty('display');
  829. } else if (originalDisplay !== undefined) {
  830. previewedElement.style.setProperty('display', originalDisplay, '');
  831. }
  832. delete previewedElement.dataset._original_display;
  833. if (previewedElement === selectedEl) {
  834. previewedElement.classList.add('selected-element');
  835. }
  836. } catch (e) {
  837. console.warn(SCRIPT_ID, "Error resetting preview style:", e)
  838. }
  839. }
  840. if (previewBtn) {
  841. previewBtn.textContent = STRINGS.preview;
  842. previewBtn.classList.remove('tertiary');
  843. previewBtn.classList.add('secondary');
  844. }
  845. isPreviewHidden = false;
  846. previewedElement = null;
  847. }
  848.  
  849. function updateInfo() {
  850. if (!info) return;
  851. const selectorText = selectedEl ? generateSelector(selectedEl, 7, false) : '';
  852. info.textContent = selectorText;
  853. infoLabel.style.display = 'block';
  854. }
  855.  
  856. let activePanel = null;
  857.  
  858. function setPanelVisibility(panelElement, visible) {
  859. if (!panelElement) return;
  860.  
  861. if (visible) {
  862. [panel, settingsPanel, listPanel].forEach(p => {
  863. if (p && p !== panelElement && p.classList.contains('visible')) {
  864. p.classList.remove('visible');
  865. const transitionEndHandler = () => {
  866. if (!p.classList.contains('visible')) p.style.display = 'none';
  867. p.removeEventListener('transitionend', transitionEndHandler);
  868. };
  869. p.addEventListener('transitionend', transitionEndHandler);
  870. setTimeout(() => {
  871. if (!p.classList.contains('visible')) p.style.display = 'none';
  872. p.removeEventListener('transitionend', transitionEndHandler);
  873. }, 350);
  874. }
  875. });
  876.  
  877. activePanel = panelElement;
  878. panelElement.style.display = 'block';
  879. requestAnimationFrame(() => {
  880. requestAnimationFrame(() => {
  881. panelElement.classList.add('visible');
  882. });
  883. });
  884. } else {
  885. if (activePanel === panelElement) activePanel = null;
  886. panelElement.classList.remove('visible');
  887. const transitionEndHandler = () => {
  888. if (!panelElement.classList.contains('visible')) panelElement.style.display = 'none';
  889. panelElement.removeEventListener('transitionend', transitionEndHandler);
  890. };
  891. panelElement.addEventListener('transitionend', transitionEndHandler);
  892. setTimeout(() => {
  893. if (!panelElement.classList.contains('visible')) panelElement.style.display = 'none';
  894. panelElement.removeEventListener('transitionend', transitionEndHandler);
  895. }, 350);
  896. }
  897. }
  898.  
  899. async function addBlockRule(selector) {
  900. console.log('[addBlockRule] Attempting for selector:', selector);
  901. if (!selector) {
  902. return {
  903. success: false,
  904. message: STRINGS.cannotGenerateSelector
  905. };
  906. }
  907.  
  908. let fullSelector = "##" + selector;
  909. if (settings.includeSiteName) {
  910. const hostname = location.hostname;
  911. if (!hostname) {
  912. console.error(SCRIPT_ID, "Could not get location.hostname");
  913. return {
  914. success: false,
  915. message: '호스트 이름을 가져올 수 없습니다.'
  916. };
  917. }
  918. fullSelector = hostname + fullSelector;
  919. }
  920.  
  921. if (blockedSelectorsCache.includes(fullSelector)) {
  922. console.log(SCRIPT_ID, "Rule already exists:", fullSelector);
  923. return {
  924. success: false,
  925. message: STRINGS.ruleExists
  926. };
  927. }
  928.  
  929. const updatedList = [...blockedSelectorsCache, fullSelector];
  930. await saveBlockedSelectors(updatedList);
  931.  
  932. console.log(SCRIPT_ID, "Rule added:", fullSelector);
  933. return {
  934. success: true,
  935. rule: fullSelector
  936. };
  937. }
  938.  
  939. async function showList() {
  940. console.log('[showList] Function called');
  941. try {
  942. const arr = await loadBlockedSelectors();
  943. console.log(`[showList] Rendering ${arr.length} rules.`);
  944. listContainer.innerHTML = '';
  945.  
  946. if (arr.length === 0) {
  947. listContainer.innerHTML = `<p id="blocklist-empty">${STRINGS.noRules}</p>`;
  948. } else {
  949. arr.forEach((rule, index) => {
  950. const item = document.createElement('div');
  951. item.className = 'blocklist-item';
  952.  
  953. const span = document.createElement('span');
  954. span.textContent = rule;
  955. span.title = rule;
  956.  
  957. const controlsDiv = document.createElement('div');
  958. controlsDiv.className = 'blocklist-controls';
  959.  
  960. const copyButton = document.createElement('button');
  961. copyButton.className = 'mb-btn blocklist-btn blocklist-btn-copy';
  962. copyButton.textContent = STRINGS.copy;
  963. copyButton.title = '규칙 복사';
  964. copyButton.addEventListener('click', () => {
  965. try {
  966. GM_setClipboard(rule);
  967. showToast(STRINGS.ruleCopied, 'success', 2000);
  968. } catch (copyError) {
  969. console.error(SCRIPT_ID, "Error copying rule to clipboard:", copyError);
  970. showToast(STRINGS.clipboardError, 'error');
  971. }
  972. });
  973.  
  974. const deleteButton = document.createElement('button');
  975. deleteButton.className = 'mb-btn blocklist-btn blocklist-btn-delete';
  976. deleteButton.textContent = '삭제';
  977. deleteButton.title = '규칙 삭제';
  978. deleteButton.addEventListener('click', async () => {
  979. console.log('[showList] Delete button clicked for rule:', rule);
  980. try {
  981. const currentIndex = blockedSelectorsCache.indexOf(rule);
  982. if (currentIndex > -1) {
  983. blockedSelectorsCache.splice(currentIndex, 1);
  984. await saveBlockedSelectors(blockedSelectorsCache);
  985.  
  986. item.style.opacity = '0';
  987. item.style.transform = 'translateX(20px) scale(0.95)';
  988. setTimeout(async () => {
  989. item.remove();
  990. if (listContainer.childElementCount === 0) {
  991. listContainer.innerHTML = `<p id="blocklist-empty">${STRINGS.noRules}</p>`;
  992. }
  993. await applyBlocking(false);
  994. showToast(STRINGS.ruleDeleted, 'info', 2000);
  995. }, 300);
  996.  
  997. } else {
  998. console.warn("Rule not found in cache for deletion:", rule);
  999. showToast(STRINGS.ruleDeleteError, 'error');
  1000. await showList();
  1001. }
  1002. } catch (deleteError) {
  1003. console.error(SCRIPT_ID, "Error deleting rule:", deleteError);
  1004. showToast(STRINGS.ruleDeleteError, 'error');
  1005. }
  1006. });
  1007.  
  1008. controlsDiv.append(copyButton, deleteButton);
  1009. item.append(span, controlsDiv);
  1010. listContainer.append(item);
  1011. });
  1012. }
  1013. console.log('[showList] Rendering list panel.');
  1014. setPanelVisibility(listPanel, true);
  1015.  
  1016. } catch (error) {
  1017. console.error(SCRIPT_ID, "Error in showList:", error);
  1018. showToast(STRINGS.listShowError, 'error');
  1019. setPanelVisibility(listPanel, false);
  1020. }
  1021. }
  1022.  
  1023. function applyGradientMask(container) {
  1024. if (!container) return;
  1025. const updateMask = () => {
  1026. const {
  1027. scrollTop,
  1028. scrollHeight,
  1029. clientHeight
  1030. } = container;
  1031. const isAtTop = scrollTop <= 0;
  1032. const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
  1033. if (isAtTop && isAtBottom) {
  1034. container.style.webkitMaskImage = 'none';
  1035. container.style.maskImage = 'none';
  1036. } else if (isAtTop) {
  1037. container.style.webkitMaskImage = `linear-gradient(to bottom, black 0%, black 90%, transparent 100%)`;
  1038. container.style.maskImage = container.style.webkitMaskImage;
  1039. } else if (isAtBottom) {
  1040. container.style.webkitMaskImage = `linear-gradient(to bottom, transparent 0%, black 10%, black 100%)`;
  1041. container.style.maskImage = container.style.webkitMaskImage;
  1042. } else {
  1043. container.style.webkitMaskImage = `linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%)`;
  1044. container.style.maskImage = container.style.webkitMaskImage;
  1045. }
  1046. };
  1047.  
  1048. container.addEventListener('scroll', updateMask);
  1049. requestAnimationFrame(updateMask);
  1050. }
  1051.  
  1052. function setBlockMode(enabled) {
  1053. if (!toggleBtn || !panel) return;
  1054.  
  1055. selecting = enabled;
  1056. toggleBtn.classList.toggle('selecting', enabled);
  1057. updateToggleIcon();
  1058.  
  1059. if (enabled) {
  1060. setPanelVisibility(panel, true);
  1061. if (selectedEl) {
  1062. selectedEl.classList.add('selected-element');
  1063. }
  1064. updateInfo();
  1065. } else {
  1066. setPanelVisibility(panel, false);
  1067. if (activePanel === listPanel) setPanelVisibility(listPanel, false);
  1068. if (activePanel === settingsPanel) setPanelVisibility(settingsPanel, false);
  1069.  
  1070. removeSelectionHighlight();
  1071. resetPreview();
  1072. initialTouchedElement = null;
  1073. }
  1074. console.log(SCRIPT_ID, "Selection mode:", enabled ? "ON" : "OFF");
  1075. }
  1076.  
  1077.  
  1078. console.log(SCRIPT_ID, 'Attaching event listeners...');
  1079.  
  1080. toggleBtn.addEventListener('click', () => {
  1081. setBlockMode(!selecting);
  1082. });
  1083.  
  1084. copyBtn.addEventListener('click', () => {
  1085. if (!selectedEl) {
  1086. showToast(STRINGS.noElementSelected, 'warning');
  1087. return;
  1088. }
  1089. const selector = generateSelector(selectedEl, 7, true);
  1090. if (!selector) {
  1091. showToast(STRINGS.cannotGenerateSelector, 'error');
  1092. return;
  1093. }
  1094.  
  1095. let finalSelector = "##" + selector;
  1096. if (settings.includeSiteName) {
  1097. finalSelector = location.hostname + finalSelector;
  1098. }
  1099. try {
  1100. GM_setClipboard(finalSelector);
  1101. showToast(STRINGS.selectorCopied, 'success');
  1102. } catch (err) {
  1103. console.error(SCRIPT_ID, "Error copying to clipboard:", err);
  1104. showToast(STRINGS.clipboardError, 'error');
  1105. try {
  1106. prompt(STRINGS.promptCopy, finalSelector);
  1107. } catch (e) {}
  1108. }
  1109. });
  1110.  
  1111. previewBtn.addEventListener('click', () => {
  1112. if (!selectedEl) {
  1113. showToast(STRINGS.noElementSelected, 'warning');
  1114. return;
  1115. }
  1116.  
  1117. if (!isPreviewHidden) {
  1118. if (window.getComputedStyle(selectedEl).display === 'none') {
  1119. showToast(STRINGS.alreadyHidden, 'info');
  1120. return;
  1121. }
  1122. const currentDisplay = selectedEl.style.display;
  1123. selectedEl.dataset._original_display = currentDisplay === '' ? 'unset' : currentDisplay;
  1124. selectedEl.style.setProperty('display', 'none', 'important');
  1125.  
  1126. previewBtn.textContent = STRINGS.restorePreview;
  1127. previewBtn.classList.remove('secondary');
  1128. previewBtn.classList.add('tertiary');
  1129. isPreviewHidden = true;
  1130. previewedElement = selectedEl;
  1131. selectedEl.classList.remove('selected-element');
  1132. console.log(SCRIPT_ID, "Previewing hide for:", selectedEl);
  1133.  
  1134. } else {
  1135. if (previewedElement && previewedElement !== selectedEl) {
  1136. showToast(STRINGS.previewDifferentElement, 'warning');
  1137. return;
  1138. }
  1139. resetPreview();
  1140. console.log(SCRIPT_ID, "Restored preview for:", previewedElement);
  1141. }
  1142. });
  1143.  
  1144. addBtn.addEventListener('click', async () => {
  1145. console.log('[addBtn] Clicked');
  1146. if (!selectedEl) {
  1147. showToast(STRINGS.noElementSelected, 'warning');
  1148. return;
  1149. }
  1150.  
  1151. try {
  1152. const selector = generateSelector(selectedEl, 7, true);
  1153. console.log('[addBtn] Generated selector for saving:', selector);
  1154. if (!selector) {
  1155. showToast(STRINGS.cannotGenerateSelector, 'error');
  1156. return;
  1157. }
  1158.  
  1159. const result = await addBlockRule(selector);
  1160. console.log('[addBtn] addBlockRule result:', result);
  1161.  
  1162. if (result.success) {
  1163. showToast(STRINGS.ruleSavedReloading, 'success', 2000);
  1164. try {
  1165. const ruleSelector = result.rule.split('##')[1];
  1166. document.querySelectorAll(ruleSelector).forEach(el => {
  1167. if (!originalDisplayMap.has(el)) {
  1168. originalDisplayMap.set(el, el.style.display || 'unset');
  1169. }
  1170. el.style.setProperty('display', 'none', 'important');
  1171. el.setAttribute('data-mes-hidden', 'true');
  1172. });
  1173. } catch (applyError) {
  1174. console.error(SCRIPT_ID, "Error applying rule immediately after save:", applyError);
  1175. showToast(STRINGS.ruleSavedApplyFailed, 'warning', 3000);
  1176. }
  1177. setBlockMode(false);
  1178.  
  1179. } else {
  1180. showToast(result.message || STRINGS.ruleAddError, result.success ? 'success' : 'info');
  1181. }
  1182. } catch (error) {
  1183. console.error(SCRIPT_ID, "Error during Save Rule click:", error);
  1184. showToast(`${STRINGS.ruleAddError} ${error.message}`, 'error');
  1185. }
  1186. });
  1187.  
  1188. listBtn.addEventListener('click', () => {
  1189. console.log('[listBtn] Clicked');
  1190. setPanelVisibility(panel, false);
  1191. showList();
  1192. });
  1193.  
  1194. settingsBtn.addEventListener('click', () => {
  1195. console.log('[settingsBtn] Clicked');
  1196. setPanelVisibility(panel, false);
  1197. setPanelVisibility(settingsPanel, true);
  1198. });
  1199.  
  1200. cancelBtn.addEventListener('click', () => {
  1201. setBlockMode(false);
  1202. });
  1203.  
  1204. listClose.addEventListener('click', () => {
  1205. console.log('[listClose] Clicked');
  1206. setPanelVisibility(listPanel, false);
  1207. if (selecting) {
  1208. console.log('[listClose] Restoring main panel');
  1209. setPanelVisibility(panel, true);
  1210. }
  1211. });
  1212.  
  1213. settingsClose.addEventListener('click', () => {
  1214. console.log('[settingsClose] Clicked');
  1215. setPanelVisibility(settingsPanel, false);
  1216. if (selecting) {
  1217. console.log('[settingsClose] Restoring main panel');
  1218. setPanelVisibility(panel, true);
  1219. }
  1220. });
  1221.  
  1222. toggleSiteBtn.addEventListener('click', async () => {
  1223. settings.includeSiteName = !settings.includeSiteName;
  1224. toggleSiteBtn.textContent = settings.includeSiteName ? STRINGS.on : STRINGS.off;
  1225. toggleSiteBtn.classList.toggle('active', settings.includeSiteName);
  1226. await saveSettings();
  1227. showToast(STRINGS.settingsSaved, 'info', 1500);
  1228. });
  1229.  
  1230. adguardLogoToggleBtn.addEventListener('click', async () => {
  1231. settings.showAdguardLogo = !settings.showAdguardLogo;
  1232. adguardLogoToggleBtn.textContent = settings.showAdguardLogo ? STRINGS.on : STRINGS.off;
  1233. adguardLogoToggleBtn.classList.toggle('active', settings.showAdguardLogo);
  1234. updateToggleIcon();
  1235. await saveSettings();
  1236. showToast(STRINGS.settingsSaved, 'info', 1500);
  1237. });
  1238.  
  1239. tempDisableBtn.addEventListener('click', async () => {
  1240. settings.tempBlockingDisabled = !settings.tempBlockingDisabled;
  1241. tempDisableBtn.textContent = settings.tempBlockingDisabled ? STRINGS.on : STRINGS.off;
  1242. tempDisableBtn.classList.toggle('active', settings.tempBlockingDisabled);
  1243. tempDisableBtn.classList.toggle('error', settings.tempBlockingDisabled);
  1244. tempDisableBtn.classList.toggle('secondary', !settings.tempBlockingDisabled);
  1245.  
  1246. if (settings.tempBlockingDisabled) {
  1247. disableAllBlocking();
  1248. } else {
  1249. await enableAllBlocking();
  1250. }
  1251. await saveSettings();
  1252. });
  1253.  
  1254. const updateCornerButtons = (activeCorner) => {
  1255. cornerButtons.forEach(btn => {
  1256. btn.classList.toggle('active', btn.dataset.corner === activeCorner);
  1257. });
  1258. };
  1259.  
  1260. cornerButtons.forEach(button => {
  1261. button.addEventListener('click', async () => {
  1262. const selectedCorner = button.dataset.corner;
  1263. if (settings.toggleBtnCorner !== selectedCorner) {
  1264. settings.toggleBtnCorner = selectedCorner;
  1265. updateCornerButtons(selectedCorner);
  1266. applyToggleBtnPosition();
  1267. await saveSettings();
  1268. showToast(STRINGS.settingsSaved, 'info', 1500);
  1269. }
  1270. });
  1271. });
  1272.  
  1273. updateCornerButtons(settings.toggleBtnCorner);
  1274.  
  1275.  
  1276. let saveTimeout;
  1277. const debounceSaveSettings = () => {
  1278. clearTimeout(saveTimeout);
  1279. saveTimeout = setTimeout(async () => {
  1280. await saveSettings();
  1281. console.log(SCRIPT_ID, "Settings saved via debounce");
  1282. }, 500);
  1283. };
  1284.  
  1285. panelOpacitySlider.addEventListener('input', e => {
  1286. const newValue = parseFloat(e.target.value);
  1287. settings.panelOpacity = newValue;
  1288. panelOpacityValue.textContent = newValue.toFixed(2);
  1289. document.documentElement.style.setProperty('--panel-opacity', newValue);
  1290. document.querySelectorAll('#mobile-block-panel, #mobile-settings-panel, #mobile-blocklist-panel').forEach(p => {
  1291. p.style.setProperty('background-color', `rgba(40, 43, 48, ${newValue})`, 'important');
  1292. });
  1293. debounceSaveSettings();
  1294. });
  1295.  
  1296. toggleSizeSlider.addEventListener('input', e => {
  1297. const newValue = parseFloat(e.target.value);
  1298. settings.toggleSizeScale = newValue;
  1299. toggleSizeValue.textContent = newValue.toFixed(1) + 'x';
  1300. document.documentElement.style.setProperty('--toggle-size', `${56 * newValue}px`);
  1301. if (toggleBtn) {
  1302. toggleBtn.style.setProperty('width', `var(--toggle-size)`, 'important');
  1303. toggleBtn.style.setProperty('height', `var(--toggle-size)`, 'important');
  1304. }
  1305. debounceSaveSettings();
  1306. });
  1307.  
  1308. toggleOpacitySlider.addEventListener('input', e => {
  1309. const newValue = parseFloat(e.target.value);
  1310. settings.toggleOpacity = newValue;
  1311. toggleOpacityValue.textContent = newValue.toFixed(2);
  1312. document.documentElement.style.setProperty('--toggle-opacity', newValue);
  1313. if (toggleBtn) {
  1314. toggleBtn.style.setProperty('opacity', newValue, 'important');
  1315. }
  1316. debounceSaveSettings();
  1317. });
  1318.  
  1319. backupBtn.addEventListener('click', async () => {
  1320. try {
  1321. const rules = await loadBlockedSelectors();
  1322. if (rules.length === 0) {
  1323. showToast('ℹ️ 백업할 규칙이 없습니다.', 'info');
  1324. return;
  1325. }
  1326. const jsonString = JSON.stringify(rules, null, 2);
  1327. const blob = new Blob([jsonString], {
  1328. type: 'application/json'
  1329. });
  1330. const url = URL.createObjectURL(blob);
  1331. const a = document.createElement('a');
  1332. const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
  1333. a.href = url;
  1334. a.download = `mobile_element_selector_backup_${timestamp}.json`;
  1335. document.body.appendChild(a);
  1336. a.click();
  1337. document.body.removeChild(a);
  1338. URL.revokeObjectURL(url);
  1339. showToast(STRINGS.backupStarting, 'success');
  1340. } catch (err) {
  1341. console.error(SCRIPT_ID, "Backup failed:", err);
  1342. showToast(STRINGS.backupError, 'error');
  1343. }
  1344. });
  1345.  
  1346. restoreBtn.addEventListener('click', () => {
  1347. restoreInput.click();
  1348. });
  1349.  
  1350. restoreInput.addEventListener('change', async (event) => {
  1351. const file = event.target.files[0];
  1352. if (!file) return;
  1353.  
  1354. const reader = new FileReader();
  1355. reader.onload = async (e) => {
  1356. try {
  1357. const content = e.target.result;
  1358. const parsedRules = JSON.parse(content);
  1359.  
  1360. if (!Array.isArray(parsedRules) || !parsedRules.every(item => typeof item === 'string')) {
  1361. throw new Error("Invalid file content - expected an array of strings.");
  1362. }
  1363. if (!parsedRules.every(item => item.includes('##') || parsedRules.length === 0)) {
  1364. console.warn(SCRIPT_ID, "Restored rules contain items without '##'. Proceeding anyway.");
  1365. }
  1366.  
  1367. await saveBlockedSelectors(parsedRules);
  1368. await applyBlocking(true);
  1369. showToast(STRINGS.restoreSuccess, 'success', 2500);
  1370.  
  1371. if (listPanel.classList.contains('visible')) {
  1372. await showList();
  1373. }
  1374. if (settingsPanel.classList.contains('visible')) {
  1375. tempDisableBtn.classList.toggle('active', settings.tempBlockingDisabled);
  1376. tempDisableBtn.classList.toggle('error', settings.tempBlockingDisabled);
  1377. tempDisableBtn.classList.toggle('secondary', !settings.tempBlockingDisabled);
  1378. tempDisableBtn.textContent = settings.tempBlockingDisabled ? STRINGS.on : STRINGS.off;
  1379. }
  1380.  
  1381. } catch (err) {
  1382. console.error(SCRIPT_ID, "Restore failed:", err);
  1383. if (err instanceof SyntaxError || err.message.includes("Invalid file content")) {
  1384. showToast(STRINGS.restoreErrorInvalidFile, 'error');
  1385. } else {
  1386. showToast(STRINGS.restoreErrorGeneral, 'error');
  1387. }
  1388. } finally {
  1389. restoreInput.value = '';
  1390. }
  1391. };
  1392. reader.onerror = (e) => {
  1393. console.error(SCRIPT_ID, "File reading error:", e);
  1394. showToast(STRINGS.restoreErrorGeneral, 'error');
  1395. restoreInput.value = '';
  1396. };
  1397. reader.readAsText(file);
  1398. });
  1399.  
  1400. document.addEventListener('touchstart', e => {
  1401. if (!selecting) return;
  1402.  
  1403. if (e.target.closest('.mobile-block-ui')) {
  1404. initialTouchedElement = null;
  1405. return;
  1406. }
  1407.  
  1408. const touch = e.touches[0];
  1409. touchStartX = touch.clientX;
  1410. touchStartY = touch.clientY;
  1411. touchMoved = false;
  1412.  
  1413. const potentialTarget = document.elementFromPoint(touchStartX, touchStartY);
  1414. if (potentialTarget && !potentialTarget.closest('.mobile-block-ui') && potentialTarget.tagName !== 'BODY' && potentialTarget.tagName !== 'HTML') {
  1415. initialTouchedElement = potentialTarget;
  1416. } else {
  1417. initialTouchedElement = null;
  1418. }
  1419. }, {
  1420. passive: true
  1421. });
  1422.  
  1423. document.addEventListener('touchmove', e => {
  1424. if (!selecting || touchMoved || !e.touches[0]) return;
  1425.  
  1426. if (e.target.closest('.mobile-block-ui')) return;
  1427.  
  1428.  
  1429. const touch = e.touches[0];
  1430. const dx = touch.clientX - touchStartX;
  1431. const dy = touch.clientY - touchStartY;
  1432. if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) {
  1433. touchMoved = true;
  1434. if (selectedEl) {
  1435. selectedEl.classList.remove('selected-element');
  1436. }
  1437. initialTouchedElement = null;
  1438. }
  1439. }, {
  1440. passive: true
  1441. });
  1442.  
  1443. document.addEventListener('touchend', e => {
  1444. if (!selecting) return;
  1445.  
  1446. const touchEndTarget = e.target;
  1447.  
  1448. if (touchEndTarget.closest('.mobile-block-ui .mb-btn') || touchEndTarget === toggleBtn || toggleBtn.contains(touchEndTarget)) {
  1449. touchMoved = false;
  1450. return;
  1451. }
  1452. if (touchEndTarget.closest('.mobile-block-ui')) {
  1453. touchMoved = false;
  1454. return;
  1455. }
  1456.  
  1457.  
  1458. if (!touchMoved) {
  1459. try {
  1460. e.preventDefault();
  1461. e.stopImmediatePropagation();
  1462. } catch (err) {
  1463. console.warn(SCRIPT_ID, "Could not preventDefault/stopImmediatePropagation on touchend:", err);
  1464. }
  1465. } else {
  1466. touchMoved = false;
  1467. return;
  1468. }
  1469.  
  1470. const touch = e.changedTouches[0];
  1471. if (!touch) return;
  1472.  
  1473. let targetEl = initialTouchedElement;
  1474. if (!targetEl || targetEl.closest('.mobile-block-ui')) {
  1475. targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
  1476. }
  1477. while (targetEl && (targetEl.nodeType !== 1 || targetEl.closest('.mobile-block-ui'))) {
  1478. targetEl = targetEl.parentElement;
  1479. }
  1480.  
  1481. if (targetEl && targetEl.tagName !== 'BODY' && targetEl.tagName !== 'HTML') {
  1482. removeSelectionHighlight();
  1483. resetPreview();
  1484. selectedEl = targetEl;
  1485. initialTouchedElement = selectedEl;
  1486. selectedEl.classList.add('selected-element');
  1487. if (slider) slider.value = 0;
  1488. updateInfo();
  1489. } else {
  1490. removeSelectionHighlight();
  1491. resetPreview();
  1492. updateInfo();
  1493. initialTouchedElement = null;
  1494. }
  1495. }, {
  1496. capture: true,
  1497. passive: false
  1498. });
  1499.  
  1500. slider.addEventListener('input', (e) => {
  1501. if (!initialTouchedElement) {
  1502. if (selectedEl) {
  1503. initialTouchedElement = selectedEl;
  1504. } else {
  1505. return;
  1506. }
  1507. }
  1508. resetPreview();
  1509. const level = parseInt(e.target.value, 10);
  1510. let current = initialTouchedElement;
  1511. for (let i = 0; i < level && current.parentElement; i++) {
  1512. if (['body', 'html'].includes(current.parentElement.tagName.toLowerCase()) || current.parentElement.closest('.mobile-block-ui')) {
  1513. break;
  1514. }
  1515. current = current.parentElement;
  1516. }
  1517. if (selectedEl !== current) {
  1518. if (selectedEl) {
  1519. selectedEl.classList.remove('selected-element');
  1520. }
  1521. selectedEl = current;
  1522. selectedEl.classList.add('selected-element');
  1523. updateInfo();
  1524. }
  1525. });
  1526.  
  1527. function makePanelDraggable(el) {
  1528. if (!el) return;
  1529. let startX, startY, elementStartX, elementStartY;
  1530. let dragging = false;
  1531. let movedSinceStart = false;
  1532. const dragThreshold = 5;
  1533.  
  1534. el.addEventListener('touchstart', (e) => {
  1535. const isInsideScrollable = e.target.closest('.scrollable-container');
  1536. if (isInsideScrollable) {
  1537. dragging = false;
  1538. return;
  1539. }
  1540.  
  1541. const ignore = e.target.closest('button, input, select, textarea, .blocklist-item, .mb-slider, #blocker-info, #blocklist-container');
  1542. if (ignore && el.contains(ignore)) return;
  1543.  
  1544. if (e.touches.length > 1) return;
  1545. dragging = true;
  1546. movedSinceStart = false;
  1547.  
  1548. const touch = e.touches[0];
  1549. startX = touch.clientX;
  1550. startY = touch.clientY;
  1551.  
  1552. const rect = el.getBoundingClientRect();
  1553.  
  1554. el.style.transition = 'none';
  1555. el.style.transform = 'none';
  1556. el.style.left = `${rect.left}px`;
  1557. el.style.top = `${rect.top}px`;
  1558. el.style.right = 'auto';
  1559. el.style.bottom = 'auto';
  1560.  
  1561. elementStartX = rect.left;
  1562. elementStartY = rect.top;
  1563. el.style.cursor = 'grabbing';
  1564. }, {
  1565. passive: true
  1566. });
  1567.  
  1568. el.addEventListener('touchmove', (e) => {
  1569. if (!dragging || e.touches.length > 1) return;
  1570. const touch = e.touches[0];
  1571. const dx = touch.clientX - startX;
  1572. const dy = touch.clientY - startY;
  1573.  
  1574. if (!movedSinceStart && Math.sqrt(dx * dx + dy * dy) > dragThreshold) {
  1575. movedSinceStart = true;
  1576. }
  1577. if (movedSinceStart) {
  1578. e.preventDefault();
  1579. const newX = Math.max(0, Math.min(elementStartX + dx, window.innerWidth - el.offsetWidth));
  1580. const newY = Math.max(0, Math.min(elementStartY + dy, window.innerHeight - el.offsetHeight));
  1581. el.style.left = `${newX}px`;
  1582. el.style.top = `${newY}px`;
  1583. }
  1584. }, {
  1585. passive: false
  1586. });
  1587.  
  1588. el.addEventListener('touchend', () => {
  1589. if (dragging && movedSinceStart) {
  1590. el.dataset.wasDragged = 'true';
  1591. }
  1592. dragging = false;
  1593. movedSinceStart = false;
  1594. el.style.cursor = 'grab';
  1595. });
  1596.  
  1597. el.addEventListener('touchcancel', () => {
  1598. dragging = false;
  1599. movedSinceStart = false;
  1600. el.style.cursor = 'grab';
  1601. });
  1602. }
  1603.  
  1604. makePanelDraggable(panel);
  1605. makePanelDraggable(settingsPanel);
  1606. makePanelDraggable(listPanel);
  1607.  
  1608. const settingsScrollable = settingsPanel.querySelector('.scrollable-container');
  1609. if (settingsScrollable) applyGradientMask(settingsScrollable);
  1610.  
  1611. console.log(SCRIPT_ID, 'Initialization complete.');
  1612. }
  1613.  
  1614. async function run() {
  1615. await loadSettings();
  1616. if (document.readyState === 'loading') {
  1617. document.addEventListener('DOMContentLoaded', createUIElements);
  1618. } else {
  1619. createUIElements();
  1620. }
  1621. }
  1622.  
  1623. run().catch(error => {
  1624. console.error(SCRIPT_ID, "Unhandled error during script initialization:", error);
  1625. });
  1626.  
  1627. })();