Mobile Element Selector

모바일 요소 선택기

  1. // ==UserScript==
  2. // @name Mobile Element Selector
  3. // @author ZNJXL
  4. // @version 1.6
  5. // @namespace http://tampermonkey.net/
  6. // @description 모바일 요소 선택기
  7. // @match *://*/*
  8. // @license MIT
  9. // @grant GM_setClipboard
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // ==/UserScript==
  13.  
  14. (async function() { 'use strict'; const SCRIPT_ID = "[MES v1.4.x]";
  15.  
  16. // --- 기본 설정 값 정의 ---
  17. const DEFAULT_SETTINGS = {
  18. includeSiteName: true,
  19. buttonSizeScale: 1.0,
  20. panelOpacity: 0.95,
  21. toggleSizeScale: 1.0,
  22. toggleOpacity: 1.0,
  23. showAdguardLogo: false
  24. };
  25.  
  26. // --- 설정 값 로드 및 검증 ---
  27. let includeSiteName, buttonSizeScale, panelOpacity, toggleSizeScale, toggleOpacity, showAdguardLogo;
  28. try {
  29. includeSiteName = await GM_getValue('includeSiteName', DEFAULT_SETTINGS.includeSiteName);
  30. buttonSizeScale = parseFloat(await GM_getValue('buttonSizeScale', DEFAULT_SETTINGS.buttonSizeScale));
  31. panelOpacity = parseFloat(await GM_getValue('panelOpacity', DEFAULT_SETTINGS.panelOpacity));
  32. toggleSizeScale = parseFloat(await GM_getValue('toggleSizeScale', DEFAULT_SETTINGS.toggleSizeScale));
  33. toggleOpacity = parseFloat(await GM_getValue('toggleOpacity', DEFAULT_SETTINGS.toggleOpacity));
  34. showAdguardLogo = await GM_getValue('showAdguardLogo', DEFAULT_SETTINGS.showAdguardLogo);
  35.  
  36. if (isNaN(buttonSizeScale) || buttonSizeScale < 0.5 || buttonSizeScale > 2.0) buttonSizeScale = DEFAULT_SETTINGS.buttonSizeScale;
  37. if (isNaN(panelOpacity) || panelOpacity < 0.1 || panelOpacity > 1.0) panelOpacity = DEFAULT_SETTINGS.panelOpacity;
  38. if (isNaN(toggleSizeScale) || toggleSizeScale < 0.5 || toggleSizeScale > 2.0) toggleSizeScale = DEFAULT_SETTINGS.toggleSizeScale;
  39. if (isNaN(toggleOpacity) || toggleOpacity < 0.1 || toggleOpacity > 1.0) toggleOpacity = DEFAULT_SETTINGS.toggleOpacity;
  40.  
  41. } catch(e) {
  42. includeSiteName = DEFAULT_SETTINGS.includeSiteName;
  43. buttonSizeScale = DEFAULT_SETTINGS.buttonSizeScale;
  44. panelOpacity = DEFAULT_SETTINGS.panelOpacity;
  45. toggleSizeScale = DEFAULT_SETTINGS.toggleSizeScale;
  46. toggleOpacity = DEFAULT_SETTINGS.toggleOpacity;
  47. showAdguardLogo = DEFAULT_SETTINGS.showAdguardLogo;
  48. }
  49.  
  50. const BLOCKED_SELECTORS_KEY = 'mobileBlockedSelectors';
  51.  
  52. // --- CSS 정의 ---
  53. const style = document.createElement('style');
  54. style.textContent = `
  55. :root {
  56. --panel-opacity: ${panelOpacity};
  57. --btn-padding: ${10 * buttonSizeScale}px;
  58. --btn-font-size: ${14 * buttonSizeScale}px;
  59. --btn-min-width: ${80 * buttonSizeScale}px;
  60. --toggle-size: ${40 * toggleSizeScale}px;
  61. --toggle-opacity: ${toggleOpacity};
  62. }
  63. .mobile-block-ui {
  64. z-index: 9999 !important; touch-action: manipulation !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  65. box-sizing: border-box; position: fixed !important; visibility: visible !important;
  66. }
  67. #mobile-block-panel, #mobile-settings-panel, #mobile-blocklist-panel { opacity: var(--panel-opacity) !important; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
  68. .mb-slider { width: 100%; margin: 10px 0; -webkit-appearance: none; appearance: none; background: #555; height: 8px; border-radius: 5px; outline: none; cursor: pointer; }
  69. .mb-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; background: #4CAF50; border-radius: 50%; cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
  70. .mb-slider::-moz-range-thumb { width: 20px; height: 20px; background: #4CAF50; border-radius: 50%; cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
  71. .selected-element { background-color: rgba(255, 0, 0, 0.3) !important; outline: 1px dashed red !important; box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.1); z-index: 9998 !important; transition: background-color 0.1s ease, outline 0.1s ease, box-shadow 0.1s ease; }
  72. #mobile-block-panel { bottom: 15px; left: 50%; transform: translateX(-50%); width: calc(100% - 30px); max-width: 350px; background: rgba(40, 40, 40, 0.95); color: #eee; padding: 15px; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.6); z-index: 10001 !important; border-top: 1px solid rgba(255, 255, 255, 0.1); display: none; }
  73. #mobile-settings-panel { top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 40px); max-width: 300px; background: rgba(50, 50, 50, 0.95); color: #eee; padding: 20px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.7); z-index: 10003 !important; border: 1px solid rgba(255, 255, 255, 0.15); display: none; }
  74. #mobile-block-toggleBtn {
  75. top: 15px !important;
  76. left: 15px !important;
  77. z-index: 10002 !important;
  78. background: rgba(0,0,0,var(--toggle-opacity)) !important;
  79. width: var(--toggle-size) !important;
  80. height: var(--toggle-size) !important;
  81. border-radius: 50% !important;
  82. border: none !important;
  83. cursor: pointer !important;
  84. font-size: 0 !important;
  85. box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important;
  86. transition: background 0.3s ease, transform 0.2s ease;
  87. display: flex !important; /* Ensure flexbox for centering */
  88. align-items: center !important; /* Center vertically */
  89. justify-content: center !important; /* Center horizontally */
  90. opacity: 1 !important;
  91. backface-visibility: hidden;
  92. -webkit-backface-visibility: hidden;
  93. position: fixed !important; /* Ensure it's fixed relative to the viewport */
  94. overflow: hidden !important; /* Ensure logo fits within the circle */
  95. line-height: 0 !important; /* Prevent extra space for text */
  96. }
  97. #mobile-block-toggleBtn:active { transform: scale(0.9); }
  98. #mobile-block-toggleBtn.selecting { background: rgba(255,87,34,0.8) !important; }
  99. #mobile-block-toggleBtn .button-plus {
  100. font-size: 24px !important;
  101. color: #fff !important;
  102. line-height: var(--toggle-size) !important; /* Vertically center the plus sign */
  103. }
  104. #mobile-block-toggleBtn .adguard-logo {
  105. display: block;
  106. width: 60%;
  107. height: 60%;
  108. margin: auto;
  109. color: #fff;
  110. font-size: 16px;
  111. font-weight: bold;
  112. text-align: center;
  113. line-height: 1;
  114. }
  115. .mb-btn { padding: var(--btn-padding); border: none; border-radius: 8px; color: #fff; font-size: var(--btn-font-size); cursor: pointer; transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease; background-color: #555; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2); min-width: var(--btn-min-width); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; opacity: 1 !important; }
  116. .mb-btn:active { transform: scale(0.97); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); }
  117. #blocker-copy { background: linear-gradient(145deg, #2196F3, #1976D2); } #blocker-preview { background: linear-gradient(145deg, #ff9800, #f57c00); } #blocker-add-block { background: linear-gradient(145deg, #f44336, #c62828); } #blocker-settings { background: linear-gradient(145deg, #9C27B0, #7B1FA2); } #blocker-cancel { background: linear-gradient(145deg, #607D8B, #455A64); } #settings-close { background: linear-gradient(145deg, #607D8B, #455A64); margin-top: 15px; width: 100%; } #settings-toggle-site { background: linear-gradient(145deg, #009688, #00796B); } #blocker-list { background: linear-gradient(145deg, #00BCD4, #0097A7); } #blocklist-close { background: linear-gradient(145deg, #607D8B, #455A64); margin-top: 10px; width: 100%; }
  118. .button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--btn-min-width), 1fr)); gap: 8px; margin-top: 15px; } #blocker-info-wrapper { position: relative; margin-bottom: 10px; } #blocker-info { display: block; color: #90ee90; font-size: 13px; line-height: 1.4; background-color: rgba(0,0,0,0.3); padding: 5px 8px; border-radius: 4px; word-break: break-all; min-height: 1.4em; } .settings-item { margin-bottom: 15px; } .settings-item label { display: block; font-size: 13px; color: #ccc; margin-bottom: 5px; } .settings-value { float: right; color: #fff; font-weight: bold; }
  119. #mobile-blocklist-panel { position: fixed !important; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 40px); max-width: 300px; background: rgba(50,50,50, var(--panel-opacity)) !important; color: #eee; padding: 15px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.7); z-index: 10004 !important; display: none; }
  120. .blocklist-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
  121. .blocklist-item span { flex:1; word-break: break-all; }
  122. .blocklist-btn { margin-left: 5px; }
  123. `;
  124. document.head.appendChild(style);
  125.  
  126. // --- 전역 변수 ---
  127. let selecting = false;
  128. let selectedEl = null;
  129. let initialTouchedElement = null;
  130. let touchStartX=0, touchStartY=0, touchMoved=false;
  131. const moveThreshold = 10;
  132.  
  133. // --- 함수: 차단목록 불러오기/저장 ---
  134. async function loadBlockedSelectors() {
  135. const stored = await GM_getValue(BLOCKED_SELECTORS_KEY, '[]');
  136. try { return JSON.parse(stored); } catch(e){ await GM_setValue(BLOCKED_SELECTORS_KEY,'[]'); return []; }
  137. }
  138. async function saveBlockedSelectors(list) {
  139. await GM_setValue(BLOCKED_SELECTORS_KEY, JSON.stringify(list));
  140. }
  141.  
  142. // --- 함수: 차단 목록에 있는 요소 숨기기 ---
  143. async function applyBlocking() {
  144. const blockedSelectors = await loadBlockedSelectors();
  145. blockedSelectors.forEach(selector => {
  146. try {
  147. const elements = document.querySelectorAll(selector);
  148. elements.forEach(el => {
  149. el.style.display = 'none';
  150. });
  151. } catch (e) {
  152. console.error("Error applying block rule:", selector, e);
  153. }
  154. });
  155. }
  156.  
  157. // --- UI 요소 생성 ---
  158. let panel, settingsPanel, toggleBtn, listPanel;
  159. function createUIElements() {
  160. // 메인 패널
  161. panel = document.createElement('div'); panel.id='mobile-block-panel'; panel.className='mobile-block-ui'; panel.style.display='none';
  162. panel.innerHTML = `
  163. <div id="blocker-info-wrapper"><span style="font-size: 12px; color: #ccc;">선택된 요소:</span><span id="blocker-info">없음</span></div>
  164. <input type="range" id="blocker-slider" class="mb-slider" min="0" max="10" value="0">
  165. <div class="button-grid">
  166. <button id="blocker-copy" class="mb-btn">복사</button>
  167. <button id="blocker-preview" class="mb-btn">미리보기</button>
  168. <button id="blocker-add-block" class="mb-btn">저장</button>
  169. <button id="blocker-list" class="mb-btn">저장된 목록</button>
  170. <button id="blocker-settings" class="mb-btn">설정</button>
  171. <button id="blocker-cancel" class="mb-btn">취소</button>
  172. </div>`;
  173. document.body.appendChild(panel);
  174.  
  175. // 차단목록 패널
  176. listPanel = document.createElement('div'); listPanel.id='mobile-blocklist-panel'; listPanel.className='mobile-block-ui';
  177. listPanel.innerHTML = `
  178. <h3 style="margin-top:0; color: #fff; text-align: center;">차단목록</h3>
  179. <div id="blocklist-container"></div>
  180. <button id="blocklist-close" class="mb-btn" style="width:100%;margin-top:10px;">닫기</button>
  181. `;
  182. document.body.appendChild(listPanel);
  183.  
  184. // 설정 패널 (기존 + AdGuard 로고 설정 추가)
  185. settingsPanel=document.createElement('div'); settingsPanel.id='mobile-settings-panel'; settingsPanel.className='mobile-block-ui'; settingsPanel.style.display='none';
  186. settingsPanel.innerHTML = `
  187. <h3 style="text-align:center; color: #fff; margin-top: 0; margin-bottom: 20px;">설정</h3>
  188. <div class="settings-item"><label for="settings-toggle-site">사이트명 포함 규칙: <button id="settings-toggle-site" class="mb-btn">${includeSiteName?"ON":"OFF"}</button></label></div>
  189. <div class="settings-item"><label for="settings-button-size">UI 크기: <span id="button-size-value" class="settings-value">${buttonSizeScale.toFixed(1)}x</span></label><input id="settings-button-size" type="range" class="mb-slider" min="0.8" max="1.5" step="0.1" value="${buttonSizeScale}"></div>
  190. <div class="settings-item"><label for="settings-panel-opacity">패널 투명도: <span id="opacity-value" class="settings-value">${panelOpacity.toFixed(2)}</span></label><input id="settings-panel-opacity" type="range" class="mb-slider" min="0.1" max="1.0" step="0.05" value="${panelOpacity}"></div>
  191. <div class="settings-item"><label for="settings-toggle-size">토글 버튼 크기: <span id="toggle-size-value" class="settings-value">${toggleSizeScale.toFixed(1)}x</span></label><input id="settings-toggle-size" type="range" class="mb-slider" min="0.5" max="2.0" step="0.1" value="${toggleSizeScale}"></div>
  192. <div class="settings-item"><label for="settings-toggle-opacity">토글 투명도: <span id="toggle-opacity-value" class="settings-value">${toggleOpacity.toFixed(2)}</span></label><input id="settings-toggle-opacity" type="range" class="mb-slider" min="0.1" max="1.0" step="0.05" value="${toggleOpacity}"></div>
  193. <div class="settings-item"><label for="settings-adguard-logo">AdGuard 로고: <button id="settings-adguard-logo" class="mb-btn">${showAdguardLogo?"ON":"OFF"}</button></label></div>
  194. <button id="settings-close" class="mb-btn" style="width:100%;margin-top:15px;">닫기</button>
  195. `;
  196. document.body.appendChild(settingsPanel);
  197.  
  198. // 토글 버튼
  199. toggleBtn=document.createElement('button'); toggleBtn.id='mobile-block-toggleBtn'; toggleBtn.className='mobile-block-ui';
  200. if (showAdguardLogo) {
  201. toggleBtn.innerHTML='<span class="adguard-logo">AG</span>';
  202. } else {
  203. toggleBtn.innerHTML='<span class="button-plus">+</span>';
  204. }
  205. document.body.appendChild(toggleBtn);
  206.  
  207. initRefs();
  208. applyBlocking(); // 초기 로드시 차단 목록 적용
  209. }
  210.  
  211. // --- 초기화: 참조 및 이벤트 ---
  212. function initRefs() {
  213. // panel refs
  214. const info=panel.querySelector('#blocker-info');
  215. const slider=panel.querySelector('#blocker-slider');
  216. const copyBtn=panel.querySelector('#blocker-copy');
  217. const previewBtn=panel.querySelector('#blocker-preview');
  218. const addBtn=panel.querySelector('#blocker-add-block');
  219. const listBtn=panel.querySelector('#blocker-list');
  220. const settingsBtn=panel.querySelector('#blocker-settings');
  221. const cancelBtn=panel.querySelector('#blocker-cancel');
  222. // list panel refs
  223. const listContainer=listPanel.querySelector('#blocklist-container');
  224. const listClose=listPanel.querySelector('#blocklist-close');
  225. // settings refs
  226. const toggleSite=settingsPanel.querySelector('#settings-toggle-site');
  227. const btnSizeSlider=settingsPanel.querySelector('#settings-button-size');
  228. const panelOpacitySlider=settingsPanel.querySelector('#settings-panel-opacity');
  229. const toggleSizeSlider=settingsPanel.querySelector('#settings-toggle-size');
  230. const toggleOpacitySlider=settingsPanel.querySelector('#settings-toggle-opacity');
  231. const btnSizeValue=settingsPanel.querySelector('#button-size-value');
  232. const panelOpacityValue=settingsPanel.querySelector('#opacity-value');
  233. const toggleSizeValue=settingsPanel.querySelector('#toggle-size-value');
  234. const toggleOpacityValue=settingsPanel.querySelector('#toggle-opacity-value');
  235. const settingsClose=settingsPanel.querySelector('#settings-close');
  236. const adguardLogoToggle = settingsPanel.querySelector('#settings-adguard-logo');
  237.  
  238. // 이벤트: 차단목록 버튼
  239. listBtn.addEventListener('click', showList);
  240. listClose.addEventListener('click', () => { listPanel.style.display='none'; });
  241.  
  242. // render 목록
  243. async function showList() {
  244. const arr=await loadBlockedSelectors();
  245. listContainer.innerHTML='';
  246. arr.forEach((rule,i)=>{
  247. const item=document.createElement('div'); item.className='blocklist-item';
  248. const span=document.createElement('span'); span.textContent=rule;
  249. const del=document.createElement('button'); del.className='mb-btn blocklist-btn'; del.textContent='삭제';
  250. const cp =document.createElement('button'); cp.className='mb-btn blocklist-btn'; cp.textContent='복사';
  251. del.addEventListener('click', async()=>{
  252. arr.splice(i,1); await saveBlockedSelectors(arr); showList(); applyBlocking(); // 삭제 후 차단 다시 적용
  253. });
  254. cp.addEventListener('click', ()=>{ GM_setClipboard(rule); alert('복사됨: '+rule); });
  255. item.append(span, del, cp); listContainer.append(item);
  256. });
  257. listPanel.style.display='block';
  258. }
  259.  
  260. // 설정 이벤트
  261. toggleSite.addEventListener('click', async()=>{
  262. includeSiteName = !includeSiteName;
  263. toggleSite.textContent = includeSiteName?'ON':'OFF';
  264. await GM_setValue('includeSiteName', includeSiteName);
  265. });
  266. btnSizeSlider.addEventListener('input', async e=>{
  267. buttonSizeScale=parseFloat(e.target.value);
  268. btnSizeValue.textContent=buttonSizeScale.toFixed(1)+'x';
  269. document.documentElement.style.setProperty('--btn-padding', `${10*buttonSizeScale}px`);
  270. document.documentElement.style.setProperty('--btn-font-size', `${14*buttonSizeScale}px`);
  271. await GM_setValue('buttonSizeScale', buttonSizeScale);
  272. });
  273. panelOpacitySlider.addEventListener('input', async e=>{
  274. panelOpacity=parseFloat(e.target.value);
  275. panelOpacityValue.textContent=panelOpacity.toFixed(2);
  276. document.documentElement.style.setProperty('--panel-opacity', panelOpacity);
  277. await GM_setValue('panelOpacity', panelOpacity);
  278. });
  279. toggleSizeSlider.addEventListener('input', async e=>{
  280. toggleSizeScale=parseFloat(e.target.value);
  281. toggleSizeValue.textContent=toggleSizeScale.toFixed(1)+'x';
  282. document.documentElement.style.setProperty('--toggle-size', `${40*toggleSizeScale}px`);
  283. await GM_setValue('toggleSizeScale', toggleSizeScale);
  284. });
  285. toggleOpacitySlider.addEventListener('input', async e=>{
  286. toggleOpacity=parseFloat(e.target.value);
  287. toggleOpacityValue.textContent=toggleOpacity.toFixed(2);
  288. document.documentElement.style.setProperty('--toggle-opacity', toggleOpacity);
  289. await GM_setValue('toggleOpacity', toggleOpacity);
  290. });
  291. settingsClose.addEventListener('click', ()=>{ settingsPanel.style.display='none'; });
  292.  
  293. adguardLogoToggle.addEventListener('click', async () => {
  294. showAdguardLogo = !showAdguardLogo;
  295. adguardLogoToggle.textContent = showAdguardLogo ? 'ON' : 'OFF';
  296. await GM_setValue('showAdguardLogo', showAdguardLogo);
  297. if (toggleBtn) {
  298. if (showAdguardLogo) {
  299. toggleBtn.innerHTML = '<span class="adguard-logo">AG</span>';
  300. } else {
  301. toggleBtn.innerHTML = '<span class="button-plus">+</span>';
  302. }
  303. }
  304. });
  305.  
  306. // 기타: 토글, 선택 등 기존 로직 연결...
  307. document.addEventListener('touchstart', e => { if (!selecting) return; const isUIElement = e.target.closest('.mobile-block-ui'); if (isUIElement) return; const touch = e.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; touchMoved = false; }, { passive: true });
  308. document.addEventListener('touchmove', e => { if (!selecting || touchMoved) return; const isUIElement = e.target.closest('.mobile-block-ui'); if (isUIElement || !e.touches[0]) return; const touch = e.touches[0]; const dx = touch.clientX - touchStartX; const dy = touch.clientY - touchStartY; if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) { touchMoved = true; if (selectedEl) selectedEl.classList.remove('selected-element'); } }, { passive: true });
  309. document.addEventListener('touchend', e => { if (!selecting) return; const isUIElement = e.target.closest('.mobile-block-ui'); if (isUIElement) return; if (touchMoved) { touchMoved = false; if (selectedEl) selectedEl.classList.add('selected-element'); return; } try { e.preventDefault(); e.stopImmediatePropagation(); } catch (err) {} const touch = e.changedTouches[0]; if (!touch) return; const targetEl = document.elementFromPoint(touch.clientX, touch.clientY); if (targetEl && !targetEl.closest('.mobile-block-ui') && targetEl !== document.body && targetEl !== document.documentElement) { removeSelectionHighlight(); resetPreview(); selectedEl = targetEl; initialTouchedElement = targetEl; selectedEl.classList.add('selected-element'); if (panel && panel.querySelector('#blocker-slider')) panel.querySelector('#blocker-slider').value = 0; updateInfo(); } else { removeSelectionHighlight(); resetPreview(); updateInfo(); } }, { capture: true, passive: false });
  310.  
  311. if (panel && panel.querySelector('#blocker-slider')) panel.querySelector('#blocker-slider').addEventListener('input', (e) => { if (!initialTouchedElement) return; resetPreview(); const level = parseInt(e.target.value, 10); let current = initialTouchedElement; for (let i = 0; i < level && current.parentElement; i++) { if (['body', 'html'].includes(current.parentElement.tagName.toLowerCase()) || current.parentElement.closest('.mobile-block-ui')) break; current = current.parentElement; } if (selectedEl !== current) { if (selectedEl) selectedEl.classList.remove('selected-element'); selectedEl = current; selectedEl.classList.add('selected-element'); updateInfo(); } });
  312. if (panel && panel.querySelector('#blocker-copy')) panel.querySelector('#blocker-copy').addEventListener('click', () => { if (!selectedEl) { alert('선택된 요소가 없습니다.'); return; } const selector = generateSelector(selectedEl); if (!selector) { alert('❌ 유효한 선택자를 생성할 수 없습니다.'); return; } let finalSelector = "##" + selector; if (includeSiteName) finalSelector = location.hostname + finalSelector; try { GM_setClipboard(finalSelector); alert('✅ 선택자가 복사되었습니다!\n' + finalSelector); } catch (err) { alert("❌ 클립보드 복사에 실패했습니다."); prompt("선택자를 직접 복사하세요:", finalSelector); } });
  313. if (panel && panel.querySelector('#blocker-preview')) panel.querySelector('#blocker-preview').addEventListener('click', () => { if (!selectedEl) { alert('선택된 요소가 없습니다.'); return; } if (!isPreviewHidden) { if (window.getComputedStyle(selectedEl).display === 'none') { alert('이미 숨겨진 요소입니다.'); return; } selectedEl.dataset._original_display = selectedEl.style.display || ''; selectedEl.style.display = 'none'; panel.querySelector('#blocker-preview').textContent = '되돌리기'; isPreviewHidden = true; previewedElement = selectedEl; selectedEl.classList.remove('selected-element'); } else if (previewedElement === selectedEl) { try { selectedEl.style.display = selectedEl.dataset._original_display || ''; delete selectedEl.dataset._original_display; } catch(e) {} panel.querySelector('#blocker-preview').textContent = '미리보기'; isPreviewHidden = false; previewedElement = null; selectedEl.classList.add('selected-element'); } else { alert('다른 요소가 선택되었습니다. 먼저 해당 요소의 미리보기를 되돌리거나 선택을 취소하세요.'); } });
  314. if (panel && panel.querySelector('#blocker-add-block')) panel.querySelector('#blocker-add-block').addEventListener('click', async () => { if (!selectedEl) { alert('선택된 요소가 없습니다.'); return; } const selector = generateSelector(selectedEl); if (!selector) { alert('❌ 유효한 선택자를 생성할 수 없습니다.'); return; } const result = await addBlockRule(selector); if (result.success) { alert(`✅ 선택된 요소가 차단되어 차단 목록으로 이동했습니다.`); applyBlocking(); } else { alert(`ℹ️ ${result.message}`); } });
  315. if (panel && panel.querySelector('#blocker-settings')) panel.querySelector('#blocker-settings').addEventListener('click', () => { settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block'; });
  316. if (panel && panel.querySelector('#blocker-cancel')) panel.querySelector('#blocker-cancel').addEventListener('click', () => { setBlockMode(false); });
  317. if (toggleBtn) toggleBtn.addEventListener('click', () => { setBlockMode(!selecting); });
  318.  
  319. function setBlockMode(enabled) { if (!toggleBtn || !panel || !settingsPanel) return; selecting = enabled; toggleBtn.classList.toggle('selecting', enabled); panel.style.display = enabled ? 'block' : 'none'; settingsPanel.style.display = 'none'; listPanel.style.display = 'none'; if (!enabled) { removeSelectionHighlight(); resetPreview(); } if (panel && panel.querySelector('#blocker-slider')) panel.querySelector('#blocker-slider').value = 0; updateInfo(); }
  320. function removeSelectionHighlight() { if (selectedEl) { selectedEl.classList.remove('selected-element'); selectedEl = null; initialTouchedElement = null; } }
  321. let isPreviewHidden = false; let previewedElement = null; function resetPreview() { if (isPreviewHidden && previewedElement) { try { previewedElement.style.display = previewedElement.dataset._original_display || ''; delete previewedElement.dataset._original_display; } catch (e) {} if (panel && panel.querySelector('#blocker-preview')) panel.querySelector('#blocker-preview').textContent = '미리보기'; isPreviewHidden = false; previewedElement = null; } else if (panel && panel.querySelector('#blocker-preview')) { panel.querySelector('#blocker-preview').textContent = '미리보기'; } }
  322. function updateInfo() { if (panel && panel.querySelector('#blocker-info')) { panel.querySelector('#blocker-info').textContent = selectedEl ? generateSelector(selectedEl) : '없음'; } }
  323. function generateSelector(el) { if (!el || el.nodeType !== 1 || el.closest('.mobile-block-ui')) return ''; const parts = []; let current = el; const maxDepth = 7; let depth = 0; while (current && current.tagName && current.tagName.toLowerCase() !== 'body' && current.tagName.toLowerCase() !== 'html' && depth < maxDepth) { const parent = current.parentElement; if (current.classList.contains('mobile-block-ui')) { current = parent; continue; } const tagName = current.tagName.toLowerCase(); let selectorPart = tagName; if (current.id && !/\d/.test(current.id)) { try { selectorPart = `#${CSS.escape(current.id)}`; parts.unshift(selectorPart); depth++; break; } catch (e) {} } if (!selectorPart.startsWith('#')) { const classes = Array.from(current.classList).filter(c => c && !c.startsWith('ember-') && !c.startsWith('react-') && !/^[a-zA-Z]{1,2}$/.test(c) && !/\d/.test(c.substring(0,1)) && !['selected-element', 'mobile-block-ui'].includes(c)); if (classes.length > 0) { try { selectorPart += '.' + classes.map(c => CSS.escape(c)).join('.'); } catch (e) {} } else if (parent && !parent.closest('.mobile-block-ui')) { const siblings = Array.from(parent.children).filter(sibling => !sibling.classList.contains('mobile-block-ui')); let sameTagIndex = 0; let currentIndex = -1; for (let i = 0; i < siblings.length; i++) { if (siblings[i].tagName === current.tagName) { sameTagIndex++; if (siblings[i] === current) { currentIndex = sameTagIndex; } } } if (currentIndex > 0 && sameTagIndex > 1) { selectorPart = `${tagName}:nth-of-type(${currentIndex})`; } } } parts.unshift(selectorPart); depth++; if (!parent || parent.tagName.toLowerCase() === 'body' || parent.tagName.toLowerCase() === 'html') break; current = parent; } let finalSelector = parts.join(' > '); const lastIdIndex = finalSelector.lastIndexOf('#'); if (lastIdIndex > 0) { finalSelector = finalSelector.substring(lastIdIndex); } else if (finalSelector.length > 150) { finalSelector = parts.slice(-2).join(' > '); } if (!finalSelector || finalSelector === 'body' || finalSelector === 'html') return ''; return finalSelector; }
  324. async function loadBlockedSelectors() { const stored = await GM_getValue(BLOCKED_SELECTORS_KEY, '[]'); try { return JSON.parse(stored); } catch(e){ await GM_setValue(BLOCKED_SELECTORS_KEY,'[]'); return []; } }
  325. async function saveBlockedSelectors(selectors) { const selectorsToSave = Array.isArray(selectors) ? selectors : []; await GM_setValue(BLOCKED_SELECTORS_KEY, JSON.stringify(selectorsToSave)); }
  326. async function addBlockRule(selector) { if (!selector) return { success: false, message: '유효하지 않은 선택자입니다.' }; let fullSelector = "##" + selector; if (includeSiteName) { const hostname = location.hostname; if (!hostname) return { success: false, message: '호스트 이름을 가져올 수 없습니다.'}; fullSelector = hostname + fullSelector; } const blockedSelectors = await loadBlockedSelectors(); if (!blockedSelectors.includes(fullSelector)) { blockedSelectors.push(fullSelector); await saveBlockedSelectors(blockedSelectors); return { success: true, rule: fullSelector }; } return { success: false, message: '이미 저장된 규칙입니다.' }; }
  327.  
  328. function makeDraggable(el) { if (!el) return; let startX, startY, dragStartX, dragStartY; let dragging = false, moved = false; let initialTransform = ''; const handleTouchStart = (e) => { const targetTag = e.target.tagName.toLowerCase(); if (['input', 'button', 'textarea', 'select'].includes(targetTag) || e.target.closest('.mb-btn, .mb-slider')) return; if (dragging) return; dragging = true; moved = false; const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; const computedStyle = window.getComputedStyle(el); dragStartX = parseFloat(computedStyle.left) || 0; dragStartY = parseFloat(computedStyle.top) || 0; initialTransform = computedStyle.transform !== 'none' ? computedStyle.transform : ''; el.style.transition = 'none'; }; const handleTouchMove = (e) => { if (!dragging) return; const touch = e.touches[0]; const dx = touch.clientX - startX; const dy = touch.clientY - startY; if (!moved && Math.sqrt(dx * dx + dy * dy) > 10) { moved = true; if (initialTransform) { el.style.transform = 'none'; } try { e.preventDefault(); } catch {} } if (moved) { let newX = dragStartX + dx; let newY = dragStartY + dy; const elWidth = el.offsetWidth; const elHeight = el.offsetHeight; const parentWidth = window.innerWidth; const parentHeight = window.innerHeight; newX = Math.max(0, Math.min(newX, parentWidth - elWidth)); newY = Math.max(0, Math.min(newY, parentHeight - elHeight)); el.style.left = newX + 'px'; el.style.top = newY + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; } }; const handleTouchEnd = (e) => { if (!dragging) return; dragging = false; el.style.transition = ''; if (moved) { try { e.preventDefault(); e.stopPropagation(); } catch {} } }; el.addEventListener('touchstart', handleTouchStart, { passive: true }); el.addEventListener('touchmove', handleTouchMove, { passive: false }); el.addEventListener('touchend', handleTouchEnd, { passive: false }); el.addEventListener('touchcancel', handleTouchEnd, { passive: false }); }
  329.  
  330. makeDraggable(panel);
  331. makeDraggable(settingsPanel);
  332. makeDraggable(toggleBtn);
  333. makeDraggable(listPanel);
  334.  
  335. }
  336.  
  337. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createUIElements);
  338. else createUIElements();
  339.  
  340. })();