דילוג לתוכן
  • חוקי הפורום
  • פופולרי
  • לא נפתר
  • משתמשים
  • חיפוש גוגל בפורום
  • צור קשר
עיצובים
  • בהיר
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • כהה
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • ברירת מחדל (ללא עיצוב (ברירת מחדל))
  • ללא עיצוב (ברירת מחדל)
כיווץ
מתמחים טופ
  1. דף הבית
  2. מחשבים וטכנולוגיה
  3. עזרה הדדית - מחשבים וטכנולוגיה
  4. להורדה | תוסף לכרום לניהול והסתרת סרגלים באתרים

להורדה | תוסף לכרום לניהול והסתרת סרגלים באתרים

מתוזמן נעוץ נעול הועבר עזרה הדדית - מחשבים וטכנולוגיה
1 פוסטים 1 כותבים 24 צפיות 1 עוקבים
  • מהישן לחדש
  • מהחדש לישן
  • הכי הרבה הצבעות
תגובה
  • תגובה כנושא
התחברו כדי לפרסם תגובה
נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
  • ר מנותק
    ר מנותק
    רוצה לדעת 3
    כתב נערך לאחרונה על ידי
    #1

    תוסף שבניתי באמצעות ידידינו פייבל,
    המאפשר הקטנת או הסתרת הסרגלים בזמן ביקור או גלילה באתר,
    התוסף מיועד לבעלי מסך קטן שמעצבן אותם שהסרגלים העליונים באתרים תופסים חלק ניכר מהמסך,
    התוסף כולל שליטה דרך לחיצה על סמל התוסף ו\או כפתור צף המציג את האפשרויות
    רגיל-קומפקטי-מוסתר-הסתר בגלילה
    אשמח לשמוע על אתרים שבהם הוא לא הצליח לתפוס את הסרגל
    התוסף ניתן או כתוסף נפרד
    header-manager-extension.zip

    או כסקריפט לטמפרמונקי

    // ==UserScript==
    // @name         Header Manager
    // @namespace    yehuda.header-manager
    // @version      4.4.0
    // @description  שליטה בסרגלים עליונים קבועים של אתרים: רגיל / קומפקטי / מוסתר / הסתר-בגלילה
    // @match        *://*/*
    // @grant        GM_getValue
    // @grant        GM_setValue
    // @grant        GM_registerMenuCommand
    // @grant        unsafeWindow
    // @noframes
    // @run-at       document-idle
    // ==/UserScript==
    
    /*
     * ============================ מגבלות ידועות (במכוון, לא לפתור כאן) ============================
     * 1. Compensating gap: אם האתר שם padding/margin מפצה על ה-body בגובה ה-header,
     *    הסתרת הסרגל (גם ב-transform) עלולה להשאיר רווח ריק. לא מטופל בגרסה זו.
     * 2. Fullscreen API דורש Transient User Activation — עובד רק מתוך pointerup/keydown/touchend
     *    ישירים. לעולם לא מגלילה. אין עקיפה, זו מדיניות אבטחה של הדפדפן.
     * 3. שורת הכתובת וסרגל הסימניות הם chrome של הדפדפן — אין שום נקודת חיבור מ-JS.
     * ============================================================================================
     */
    
    (function () {
        'use strict';
    
        // ------------------------------------------------------------------ קונפיגורציה
    
        const UI_HOST_ID = 'header-manager-ui';
    
        // סלקטורים ממוקדים בלבד — לא סורקים כל div/section (נבדק: יקר מאוד, ~2.5s על 5000 אלמנטים)
        const SELECTORS = [
            'header', 'nav', '[role="banner"]', '[role="navigation"]',
            '[class*="header" i]', '[class*="navbar" i]', '[class*="toolbar" i]', '[class*="top-bar" i]',
            '[id*="header" i]', '[id*="navbar" i]',
            // v4.1 — לפי בדיקה בפועל: יוטיוב (#masthead-container), אלמנטור/וורדפרס
            // (elementor-sticky--active מתווסף רק אחרי גלילה), בוטסטרפ (.fixed-top)
            '[id*="masthead" i]', '[class*="masthead" i]',
            '[class*="sticky" i]', '[id*="sticky" i]',
            '[class*="fixed-top" i]',
        ].join(', ');
    
        // "סריקה עמוקה" — כל האלמנטים עד עומק 6 מתחת ל-body, בלי תלות בשמות id/class.
        // opt-in פר-דומיין (כפתור בתפריט) כי זה יקר מהסלקטורים הממוקדים; אחרי שנמצא
        // משהו בדומיין, הדגל נשמר והסריקה העמוקה רצה שם אוטומטית מאז.
        const DEEP_SELECTOR = (() => {
            const parts = [];
            let p = 'body';
            for (let i = 0; i < 6; i++) { p += ' > *'; parts.push(p); }
            return parts.join(', ');
        })();
    
        // בלוקליסט על id/class בלבד (לא על role — <header role="banner"> הוא HTML5 תקני!)
        const BLOCKLIST = /cookie|consent|gdpr|banner|notice|privacy|alert|popup|modal|widget|toast|newsletter|subscribe/i;
    
        const MAX_TOP = 60;            // rect.top מקסימלי כדי להיחשב "עליון"
        const MIN_WIDTH_RATIO = 0.4;   // רוחב מינימלי ביחס לחלון
        const MIN_HEIGHT = 15;
        // תקרת גובה דינמית (v4.1): ה-header של ערוץ יוטיוב הוא 343px, אז תקרה קשיחה
        // של 250px פספסה אותו. מרשים עד 45% מגובה החלון, מינימום 250px.
        function getMaxHeight() { return Math.max(250, Math.round(window.innerHeight * 0.45)); }
    
        const DEBOUNCE_MS = 250;       // debounce ל-Observer ול-resize
        const SCROLL_JITTER = 5;       // סף רעד לגלילה
        const SCROLL_MIN_Y = 80;       // לא מסתירים לפני שגללו לפחות ככה
        const HIDE_BUFFER = 8;         // באפר לצל/box-shadow ביציאה מהמסך
        const LONG_PRESS_MS = 350;     // כניסה למצב גרירה
        const MOVE_CANCEL_PX = 6;      // תזוזה שמבטלת כניסה לגרירה
        const DBL_WINDOW_MS = 300;     // חלון לחיצה-כפולה (מונע פליקר תפריט — באג #3)
        const ESC_WINDOW_MS = 600;     // חלון ל-Esc כפול (איפוס חירום)
        // v4.3: קומפקטי = הקטנה אמיתית ב-scale (לא חיתוך ב-max-height) — הסרגל
        // מוקטן לגובה יעד של ~40px, עם רצפת scale כדי שהתוכן יישאר קריא
        const COMPACT_TARGET = 40;
        const COMPACT_MIN_SCALE = 0.35;
    
        // v4.2: העיצוב מוחל דרך class-ים + stylesheet מוזרק, לא דרך inline style.
        // הסיבה: יוטיוב (ואתרים דומים) כותבים בעצמם inline transform על ה-header
        // בכל גלילה (אנימציית הקריסה המקורית של tp-yt-app-header) — וכתיבה כזו דורסת
        // inline שלנו. כלל stylesheet עם !important מנצח inline בלי !important,
        // אז הכתיבה שלהם מפסיקה להשפיע כל עוד ה-class שלנו על האלמנט.
        // בונוס: אין יותר צורך ב-snapshots — הסרת ה-class משחזרת את המקור נקי.
        const CLS = {
            hidden:  'hdrmgr-hidden',
            compact: 'hdrmgr-compact',
            slide:   'hdrmgr-slide',
            up:      'hdrmgr-up',
        };
        const STYLE_ID = 'hdrmgr-styles';
        const MODE_CSS = `
            .${CLS.hidden}  { display: none !important; }
            .${CLS.compact} { transform: scale(var(--hdrmgr-scale, 0.6)) !important;
                              transform-origin: top center !important;
                              transition: transform 0.2s ease !important; }
            .${CLS.slide}   { transition: transform 0.2s ease !important; }
            .${CLS.up}      { transform: translateY(var(--hdrmgr-dist, -110%)) !important; }
        `;
    
        const MODES = [
            { key: 'normal',  label: 'רגיל' },
            { key: 'compact', label: 'קומפקטי' },
            { key: 'hidden',  label: 'מוסתר' },
            { key: 'auto',    label: 'הסתר בגלילה' },
        ];
    
        // ------------------------------------------------------------------ מצב (state)
    
        let targetHeaders = [];                    // מערך — תמיכה בכמה סרגלים בו-זמנית
        const hideDistances = new Map();           // el -> px  (מרחק הסתרה פר-אלמנט)
        const scaleFactors = new Map();            // el -> scale (מקדם הקטנה פר-אלמנט, כמחרוזת)
        let modeStyleEl = null;                    // ה-stylesheet המוזרק של מצבי התצוגה
        let currentMode = 'normal';
        let autoState = 'visible';                 // 'visible' | 'hidden' (רלוונטי רק במצב auto)
        let lastY = 0;
        let scrollTicking = false;
    
        let uiHost = null, shadow = null, btn = null, menu = null, previewEl = null, statusEl = null;
    
        // ------------------------------------------------------------------ עזרים
    
        // ב-SVG, className הוא SVGAnimatedString ולא string — regex ישיר עליו קורס
        function getSafeClassName(el) {
            const c = el.className;
            if (typeof c === 'string') return c;
            if (c && typeof c.baseVal === 'string') return c.baseVal;
            return '';
        }
    
        function debounce(fn, ms) {
            let t = null;
            return function (...args) {
                clearTimeout(t);
                t = setTimeout(() => fn.apply(this, args), ms);
            };
        }
    
        // השוואת זהות (references), לא כמות — תיקון באג #1:
        // באתרי SPA ה-header מוחלף ב-node חדש בלי שהכמות משתנה.
        function sameHeaderSet(a, b) {
            if (a.length !== b.length) return false;
            return a.every(el => b.includes(el));
        }
    
        // ------------------------------------------------------------------ מנוע זיהוי
    
        function collectNodes(selector) {
            try { return document.querySelectorAll(selector); } catch (e) { return []; }
        }
    
        function isDeepDomain() {
            try { return !!GM_getValue('deepscan:' + location.hostname, false); } catch (e) { return false; }
        }
    
        function findAllHeaders(forceDeep = false) {
            const seen = new Set();
            const nodes = [];
            for (const el of collectNodes(SELECTORS)) {
                if (!seen.has(el)) { seen.add(el); nodes.push(el); }
            }
            if (forceDeep || isDeepDomain()) {
                for (const el of collectNodes(DEEP_SELECTOR)) {
                    if (!seen.has(el)) { seen.add(el); nodes.push(el); }
                }
            }
    
            const candidates = [];
            for (const el of nodes) {
                // self-exclusion: הממשק שלנו (ל-host יש id שמכיל "header" — חייבים לסנן במפורש)
                if (uiHost && (el === uiHost || uiHost.contains(el))) continue;
                if (el.closest && el.closest('#' + UI_HOST_ID)) continue;
    
                // אלמנט שכבר מנוהל על ידינו וחי ב-DOM נשאר בסט בלי בדיקות גיאומטריה:
                // במצב hidden (display:none) המידות שלו אפס, וסריקה חוזרת הייתה "מאבדת"
                // אותו ואז השחזור ל-normal לא היה מוצא אותו. הוא כבר עבר את הקריטריונים כשהיה נקי.
                if (targetHeaders.includes(el) && document.contains(el)) {
                    candidates.push(el);
                    continue;
                }
    
                // בלוקליסט — על id/class בלבד
                const idClass = (el.id || '') + ' ' + getSafeClassName(el);
                if (BLOCKLIST.test(idClass)) continue;
    
                const cs = getComputedStyle(el);
                if (cs.position !== 'fixed' && cs.position !== 'sticky') continue;
    
                const rect = el.getBoundingClientRect();
                if (rect.top > MAX_TOP) continue;
                // v4.4: תקרת גובה מקלה לאלמנטים עם "אות חזק" בשם עצמו — ה-header של
                // ערוץ יוטיוב (tp-yt-app-header, ‎329px) גבוה מהתקרה הרגילה בחלונות
                // נמוכים, אבל כשהשם כבר אומר header/masthead הסיכון לטעות נמוך
                const sig = (el.tagName + ' ' + (el.id || '') + ' ' + getSafeClassName(el)).toLowerCase();
                const cap = /header|masthead|navbar|toolbar|top-?bar/.test(sig)
                    ? Math.max(420, window.innerHeight * 0.75)
                    : getMaxHeight();
                if (rect.height < MIN_HEIGHT || rect.height > cap) continue;
                if (el.offsetWidth < window.innerWidth * MIN_WIDTH_RATIO) continue;
    
                candidates.push(el);
            }
    
            // שמירת החיצוני ביותר בלבד — לא מטפלים פעמיים ב-nav שמקונן בתוך header
            return candidates.filter(el =>
                !candidates.some(other => other !== el && other.contains(el))
            );
        }
    
        // ------------------------------------------------------------------ stylesheet מצבים
    
        function ensureStylesheet() {
            if (!modeStyleEl) {
                modeStyleEl = document.createElement('style');
                modeStyleEl.id = STYLE_ID;
                modeStyleEl.textContent = MODE_CSS;
            }
            // אתרי SPA לפעמים מחליפים את <head> — מחזירים את ה-style אם הועף
            if (!modeStyleEl.isConnected) {
                (document.head || document.documentElement).appendChild(modeStyleEl);
            }
        }
    
        function removeOurClasses(el) {
            el.classList.remove(CLS.hidden, CLS.compact, CLS.slide, CLS.up);
            el.style.removeProperty('--hdrmgr-dist');
            el.style.removeProperty('--hdrmgr-scale');
        }
    
        // ניקוי entries של אלמנטים שכבר לא ב-DOM — המקבילה של תיקון באג #2 המקורי
        // (ב-v4.2 אין יותר snapshots; נשאר רק hideDistances)
        function pruneStaleEntries() {
            for (const el of [...hideDistances.keys()]) {
                if (!document.contains(el)) hideDistances.delete(el);
            }
            for (const el of [...scaleFactors.keys()]) {
                if (!document.contains(el)) scaleFactors.delete(el);
            }
        }
    
        // ------------------------------------------------------------------ מצבי תצוגה
    
        function applyMode(mode, { save = true } = {}) {
            currentMode = mode;
            ensureStylesheet();
    
            // תמיד מנקים קודם — מעבר ישיר בין מצבים לא משאיר שאריות class/var
            for (const el of targetHeaders) removeOurClasses(el);
    
            if (mode === 'compact') {
                // מודדים קודם (אחרי הניקוי, כשהסרגל בגודל מקורי) ורק אז מקטינים
                measureCompactScales();
                for (const el of targetHeaders) el.classList.add(CLS.compact);
            } else if (mode === 'hidden') {
                for (const el of targetHeaders) el.classList.add(CLS.hidden);
            } else if (mode === 'auto') {
                autoState = 'visible';
                lastY = window.scrollY;
                // transform ולא display:none — לא משנה layout flow ונותן תחושת החלקה
                for (const el of targetHeaders) el.classList.add(CLS.slide);
                measureHideDistances();
            }
            // mode === 'normal': הניקוי שכבר בוצע הוא כל מה שצריך
    
            if (save) {
                try { GM_setValue(location.hostname, mode); } catch (e) { /* ignore */ }
            }
            updatePreview();
            updateMenuSelection();
        }
    
        // ---------------------------------------------- מצב "הסתר בגלילה" (auto)
    
        // הפתרון לכמה סרגלים: translateY(-100%) מזיז כל סרגל רק בגובה של עצמו,
        // אז סרגל שני שיושב ב-top:60px היה נשאר גלוי חלקית. במקום זה מודדים לכל
        // סרגל את הקצה התחתון שלו ביחס לחלון (rect.bottom) ומזיזים אותו למעלה
        // בדיוק במרחק הזה + באפר — כך כולם יוצאים לגמרי מהמסך, בכל offset.
        // ה-translateY האפקטיבי הנוכחי של אלמנט (של האתר או שלנו) — כדי לחשב גיאומטריה
        // "נקייה" גם כשהאתר כבר קרס/הזיז את הסרגל (יוטיוב עושה את זה בכל גלילה)
        function currentTranslateY(el) {
            const t = getComputedStyle(el).transform;
            if (!t || t === 'none') return 0;
            let m = t.match(/matrix3d\(([^)]+)\)/);
            if (m) return parseFloat(m[1].split(',')[13]) || 0;
            m = t.match(/matrix\(([^)]+)\)/);
            if (m) return parseFloat(m[1].split(',')[5]) || 0;
            m = t.match(/translateY\((-?[\d.]+)px\)/);
            if (m) return parseFloat(m[1]) || 0;
            return 0;
        }
    
        function measureHideDistances() {
            for (const el of targetHeaders) {
                const rect = el.getBoundingClientRect();
                // מנטרלים transform קיים — אחרת מדידה בזמן שהאתר כבר קרס את הסרגל
                // שלו הייתה נותנת מרחק קטן מדי (הסתרה חלקית)
                const ty = currentTranslateY(el);
                const dist = Math.round(Math.max(rect.bottom - ty, rect.height) + HIDE_BUFFER);
                hideDistances.set(el, dist);
                // המרחק מוזרם ל-CSS דרך custom property — האתר לא נוגע בזה גם כשהוא
                // דורס את transform (הכלל שלנו ב-stylesheet מנצח inline בכל מקרה)
                el.style.setProperty('--hdrmgr-dist', `-${dist}px`);
            }
        }
    
        // מקדם הקטנה פר-אלמנט למצב קומפקטי: הסרגל יורד לגובה יעד (~40px),
        // אבל לא מתחת לרצפת קריאות
        function measureCompactScales() {
            for (const el of targetHeaders) {
                const rect = el.getBoundingClientRect();
                const h = Math.max(rect.height, 1);
                const scale = Math.max(COMPACT_MIN_SCALE, Math.min(1, COMPACT_TARGET / h));
                const v = String(Math.round(scale * 100) / 100);
                scaleFactors.set(el, v);
                el.style.setProperty('--hdrmgr-scale', v);
            }
        }
    
        function setAutoState(state) {
            if (autoState === state) return;
            autoState = state;
            for (const el of targetHeaders) {
                el.classList.toggle(CLS.up, state === 'hidden');
            }
            // בחזרה לגלוי — מדידה מחדש אחרי שהאנימציה מסתיימת: אם ההפעלה המקורית
            // קרתה כשהאתר החזיק את הסרגל בגובה/מיקום חריג, המרחק מתעדכן לערך הנכון
            if (state === 'visible') {
                setTimeout(() => {
                    if (currentMode === 'auto' && autoState === 'visible') measureHideDistances();
                }, 250);
            }
        }
    
        function onScroll() {
            if (currentMode !== 'auto') return;
            if (scrollTicking) return;         // rAF — לא מריצים לוגיקה על כל אירוע scroll גולמי
            scrollTicking = true;
            requestAnimationFrame(() => {
                scrollTicking = false;
                const y = window.scrollY;
                if (y > lastY + SCROLL_JITTER && y > SCROLL_MIN_Y) setAutoState('hidden');
                else if (y < lastY - SCROLL_JITTER) setAutoState('visible');
                lastY = y;
            });
        }
    
        const onResize = debounce(() => {
            if (currentMode === 'auto') {
                // מודדים מחדש רק כשהסרגלים גלויים
                setAutoState('visible');
                requestAnimationFrame(measureHideDistances);
            }
            clampHostPosition();
        }, DEBOUNCE_MS);
    
        // ------------------------------------------------------------------ Observer (SPA)
    
        function rescanNow() {
            const next = findAllHeaders();
            if (sameHeaderSet(targetHeaders, next)) return;
            // אלמנטים שיוצאים מהסט אבל עדיין ב-DOM — מנקים מהם את ה-class-ים שלנו
            for (const el of targetHeaders) {
                if (!next.includes(el) && document.contains(el)) removeOurClasses(el);
            }
            targetHeaders = next;
            pruneStaleEntries();               // באג #2
            applyMode(currentMode, { save: false });   // באג #1 — החלה מחדש על הסט החדש
        }
    
        const debouncedRescan = debounce(rescanNow, DEBOUNCE_MS);
    
        // אכיפה: מוודא שה-class-ים וה-custom property שלנו עדיין על אלמנט מנוהל.
        // אתרים כמו יוטיוב/React דורסים לפעמים את className או את style בשלמותם —
        // כאן מחזירים את מה שנמחק. בטוח מלולאות: כשהכול במקום, לא משתנה כלום.
        function enforceElement(el) {
            ensureStylesheet();
            const want = [];
            if (currentMode === 'compact') want.push(CLS.compact);
            else if (currentMode === 'hidden') want.push(CLS.hidden);
            else if (currentMode === 'auto') {
                want.push(CLS.slide);
                if (autoState === 'hidden') want.push(CLS.up);
            }
            for (const c of want) {
                if (!el.classList.contains(c)) el.classList.add(c);
            }
            if (currentMode === 'auto' && hideDistances.has(el)) {
                const v = `-${hideDistances.get(el)}px`;
                if (el.style.getPropertyValue('--hdrmgr-dist') !== v) {
                    el.style.setProperty('--hdrmgr-dist', v);
                }
            }
            if (currentMode === 'compact' && scaleFactors.has(el)) {
                const v = scaleFactors.get(el);
                if (el.style.getPropertyValue('--hdrmgr-scale') !== v) {
                    el.style.setProperty('--hdrmgr-scale', v);
                }
            }
        }
    
        function startObserver() {
            const observer = new MutationObserver((records) => {
                let needRescan = false;
                for (const r of records) {
                    const t = r.target;
                    // הממשק שלנו — לא רלוונטי
                    if (uiHost && (t === uiHost || uiHost.contains(t))) continue;
                    // שינוי attribute על סרגל מנוהל: לא rescan מלא אלא אכיפה מיידית —
                    // אם האתר דרס את ה-class/style שלנו, מחזירים אותם
                    if (r.type === 'attributes' && targetHeaders.includes(t)) {
                        enforceElement(t);
                        continue;
                    }
                    needRescan = true;
                }
                if (needRescan) debouncedRescan();
            });
            // v4.1: מאזינים גם לשינויי class/style — תופס סרגלים שנהיים fixed רק
            // אחרי גלילה דרך הוספת class (למשל elementor-sticky--active ב-letsai)
            observer.observe(document.body, {
                childList: true, subtree: true,
                attributes: true, attributeFilter: ['class', 'style'],
            });
        }
    
        // ------------------------------------------------------------------ סריקה עמוקה
    
        function runDeepScan() {
            const found = findAllHeaders(true);
            const isNew = !sameHeaderSet(targetHeaders, found);
            if (isNew && found.length) {
                // מהיום הדומיין הזה נסרק עמוק אוטומטית בכל טעינה ובכל rescan
                try { GM_setValue('deepscan:' + location.hostname, true); } catch (e) { /* ignore */ }
                targetHeaders = found;
                pruneStaleEntries();
                applyMode(currentMode, { save: false });
            }
            if (statusEl) {
                statusEl.textContent = isNew && found.length
                    ? `נמצאו ${found.length} סרגלים — סריקה עמוקה תרוץ כאן אוטומטית מעכשיו`
                    : 'לא נמצאו סרגלים נוספים';
            }
            updatePreview();
        }
    
        // ------------------------------------------------------------------ ממשק משתמש
    
        const UI_CSS = `
            * { box-sizing: border-box; margin: 0; padding: 0;
                font-family: system-ui, "Segoe UI", Arial, sans-serif; }
            #btn {
                width: 44px; height: 44px; border-radius: 50%;
                background: #1f2937; color: #fff;
                border: 2px solid rgba(255,255,255,.3);
                box-shadow: 0 2px 10px rgba(0,0,0,.4);
                font-size: 18px; line-height: 1;
                display: flex; align-items: center; justify-content: center;
                cursor: grab; user-select: none; -webkit-user-select: none;
                /* v4.1: none ולא manipulation — manipulation משאיר לדפדפן את מחוות
                   הגלילה, והוא חוטף את הגרירה באמצע (pointercancel במסך מגע).
                   none גם מנטרל את השהיית ה-300ms וה-double-tap zoom. */
                touch-action: none;
            }
            #btn.dragging { cursor: grabbing; transform: scale(1.15); }
            #menu {
                position: absolute; direction: rtl; text-align: right;
                min-width: 240px; max-width: 300px;
                background: #111827; color: #e5e7eb;
                border: 1px solid #374151; border-radius: 10px;
                padding: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.5);
                display: none; font-size: 13px;
            }
            #menu.open { display: block; }
            #menu .title { font-size: 12px; color: #9ca3af; padding: 2px 6px 6px; }
            .mode { padding: 8px 10px; border-radius: 6px; cursor: pointer; }
            .mode:hover { background: #1f2937; }
            .mode.active { background: #2563eb; color: #fff; }
            #preview {
                border-top: 1px solid #374151; margin-top: 6px; padding: 6px 6px 2px;
                font-size: 11px; color: #9ca3af; max-height: 140px; overflow: auto;
            }
            #preview .hdr { padding: 2px 0; direction: ltr; text-align: right;
                            unicode-bidi: plaintext; }
            #preview .none { color: #6b7280; font-style: italic; }
            .hint { font-size: 10px; color: #6b7280; padding: 6px 6px 0;
                    border-top: 1px solid #374151; margin-top: 6px; }
        `;
    
        function buildUI() {
            uiHost = document.createElement('div');
            uiHost.id = UI_HOST_ID;
            uiHost.style.setProperty('position', 'fixed', 'important');
            uiHost.style.setProperty('z-index', '2147483647', 'important'); // מקסימלי, דטרמיניסטי
            uiHost.style.setProperty('width', '44px', 'important');
            uiHost.style.setProperty('height', '44px', 'important');
    
            // מיקום גלובלי שמור (לא פר-דומיין)
            const left = GM_getValue('ui_global_left', null);
            const top = GM_getValue('ui_global_top', null);
            uiHost.style.setProperty('left',
                (left !== null ? left : window.innerWidth - 64) + 'px', 'important');
            uiHost.style.setProperty('top',
                (top !== null ? top : Math.round(window.innerHeight * 0.6)) + 'px', 'important');
    
            // Shadow DOM — CSS של האתר לא יכול לשבור את הממשק
            shadow = uiHost.attachShadow({ mode: 'open' });
            const style = document.createElement('style');
            style.textContent = UI_CSS;
            shadow.appendChild(style);
    
            btn = document.createElement('div');
            btn.id = 'btn';
            btn.textContent = '▤';
            btn.title = 'Header Manager — לחיצה: תפריט · לחיצה ארוכה: גרירה · דאבל-קליק: איפוס';
            shadow.appendChild(btn);
    
            menu = document.createElement('div');
            menu.id = 'menu';
            const title = document.createElement('div');
            title.className = 'title';
            title.textContent = 'Header Manager — ' + location.hostname;
            menu.appendChild(title);
    
            for (const m of MODES) {
                const item = document.createElement('div');
                item.className = 'mode';
                item.dataset.mode = m.key;
                item.textContent = m.label;
                item.addEventListener('click', () => {
                    applyMode(m.key);
                    closeMenu();
                });
                menu.appendChild(item);
            }
    
            const deepItem = document.createElement('div');
            deepItem.className = 'mode';
            deepItem.textContent = '🔍 סריקה עמוקה';
            deepItem.title = 'סריקת כל האלמנטים בעמוד — לאתרים שהזיהוי הרגיל מפספס';
            deepItem.addEventListener('click', runDeepScan);
            menu.appendChild(deepItem);
    
            statusEl = document.createElement('div');
            statusEl.className = 'title';
            menu.appendChild(statusEl);
    
            previewEl = document.createElement('div');
            previewEl.id = 'preview';
            menu.appendChild(previewEl);
    
            const hint = document.createElement('div');
            hint.className = 'hint';
            hint.textContent = 'Esc פעמיים: איפוס · דאבל-קליק עכבר: איפוס · דאבל-טאפ: מסך מלא';
            menu.appendChild(hint);
    
            shadow.appendChild(menu);
    
            // מונע תפריט הקשר בלחיצה ארוכה במגע
            btn.addEventListener('contextmenu', e => e.preventDefault());
    
            setupPointerHandling();
    
            // נצמד ל-documentElement ולא ל-body — עמיד יותר מול SPA שמחליפים את תוכן ה-body
            document.documentElement.appendChild(uiHost);
        }
    
        function updateMenuSelection() {
            if (!menu) return;
            for (const item of menu.querySelectorAll('.mode')) {
                item.classList.toggle('active', item.dataset.mode === currentMode);
            }
        }
    
        function updatePreview() {
            if (!previewEl) return;
            previewEl.textContent = '';
            if (targetHeaders.length === 0) {
                const d = document.createElement('div');
                d.className = 'none';
                d.textContent = 'לא זוהו סרגלים בעמוד זה';
                previewEl.appendChild(d);
                return;
            }
            targetHeaders.forEach((el, i) => {
                const d = document.createElement('div');
                d.className = 'hdr';
                const h = Math.round(el.getBoundingClientRect().height) || Math.round(el.offsetHeight);
                const idPart = el.id ? '#' + el.id : '';
                const clsPart = !idPart && getSafeClassName(el)
                    ? '.' + getSafeClassName(el).trim().split(/\s+/)[0] : '';
                d.textContent = `Header #${i + 1} <${el.tagName.toLowerCase()}${idPart}${clsPart}> ${h}px`;
                previewEl.appendChild(d);
            });
        }
    
        function openMenu() {
            updatePreview();
            updateMenuSelection();
            if (statusEl) statusEl.textContent = isDeepDomain() ? 'סריקה עמוקה פעילה בדומיין הזה' : '';
            // מיקום דינמי לפי מיקום הכפתור — לא יוצא מגבולות המסך
            const rect = uiHost.getBoundingClientRect();
            menu.style.top = ''; menu.style.bottom = '';
            menu.style.left = ''; menu.style.right = '';
            if (rect.top < window.innerHeight / 2) menu.style.top = '52px';
            else menu.style.bottom = '52px';
            if (rect.left < window.innerWidth / 2) menu.style.left = '0';
            else menu.style.right = '0';
            menu.classList.add('open');
        }
    
        function closeMenu() { menu.classList.remove('open'); }
        function isMenuOpen() { return menu.classList.contains('open'); }
    
        function clampHostPosition() {
            if (!uiHost) return;
            const rect = uiHost.getBoundingClientRect();
            const left = Math.min(Math.max(4, rect.left), window.innerWidth - 48);
            const top = Math.min(Math.max(4, rect.top), window.innerHeight - 48);
            uiHost.style.setProperty('left', left + 'px', 'important');
            uiHost.style.setProperty('top', top + 'px', 'important');
        }
    
        // ------------------------------------------- גרירה + קליק/דאבל-קליק (Pointer Events)
    
        function setupPointerHandling() {
            let drag = null;            // הקשר ה-pointer הנוכחי
            let longPressTimer = null;
            let clickTimer = null;      // חלון ההמתנה לקליק שני (תיקון באג #3 — פליקר)
    
            // v4.1: שמירת מיקום רציפה תוך כדי גרירה (debounce) — גם אם הגרירה
            // נקטעת באמצע (pointercancel), המיקום האחרון לא הולך לאיבוד
            const savePosition = debounce(() => {
                const rect = uiHost.getBoundingClientRect();
                GM_setValue('ui_global_left', Math.round(rect.left));
                GM_setValue('ui_global_top', Math.round(rect.top));
            }, 250);
    
            btn.addEventListener('pointerdown', (e) => {
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                e.preventDefault();
                drag = {
                    pointerId: e.pointerId,
                    pointerType: e.pointerType,
                    startX: e.clientX, startY: e.clientY,
                    hostLeft: uiHost.getBoundingClientRect().left,
                    hostTop: uiHost.getBoundingClientRect().top,
                    canDrag: false,     // נכנסים לגרירה רק אחרי long-press
                    moved: false,       // תזוזה לפני ה-long-press => לא קליק ולא גרירה
                    dragged: false,     // הזזה בפועל בזמן גרירה
                };
                try { btn.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
                longPressTimer = setTimeout(() => {
                    if (drag && !drag.moved) {
                        drag.canDrag = true;
                        btn.classList.add('dragging');
                    }
                }, LONG_PRESS_MS);
            });
    
            btn.addEventListener('pointermove', (e) => {
                if (!drag || e.pointerId !== drag.pointerId) return;
                const dx = e.clientX - drag.startX;
                const dy = e.clientY - drag.startY;
                const dist = Math.hypot(dx, dy);
    
                if (!drag.canDrag) {
                    // תזוזה מעל הסף לפני שחלף ה-long-press — מבטלים כניסה לגרירה
                    if (dist > MOVE_CANCEL_PX) {
                        drag.moved = true;
                        clearTimeout(longPressTimer);
                    }
                    return;
                }
                // גרירה פעילה
                if (dist > 1) drag.dragged = true;
                const left = Math.min(Math.max(4, drag.hostLeft + dx), window.innerWidth - 48);
                const top = Math.min(Math.max(4, drag.hostTop + dy), window.innerHeight - 48);
                uiHost.style.setProperty('left', left + 'px', 'important');
                uiHost.style.setProperty('top', top + 'px', 'important');
                savePosition();
            });
    
            const endPointer = (e) => {
                if (!drag || e.pointerId !== drag.pointerId) return;
                clearTimeout(longPressTimer);
                btn.classList.remove('dragging');
                const d = drag;
                drag = null;
    
                if (d.canDrag) {
                    if (d.dragged) {
                        // גרירה בפועל → שמירת מיקום גלובלית
                        const rect = uiHost.getBoundingClientRect();
                        GM_setValue('ui_global_left', Math.round(rect.left));
                        GM_setValue('ui_global_top', Math.round(rect.top));
                        return;
                    }
                    // long-press בלי הזזה → מתנהג כקליק רגיל (תפריט)
                } else if (d.moved) {
                    // תזוזה בלי long-press (swipe מהיר) → לא קליק, מתעלמים
                    return;
                }
    
                // --- קליק / דאבל-קליק, מאוחד לעכבר ולמגע ---
                // תיקון באג #3: התפריט לא נפתח מיידית אלא אחרי חלון של 300ms.
                // אם מגיע קליק שני בתוך החלון — זו לחיצה כפולה, והתפריט לא מהבהב.
                if (clickTimer !== null) {
                    clearTimeout(clickTimer);
                    clickTimer = null;
                    if (d.pointerType === 'touch') {
                        // double-tap במגע → מסך מלא (pointerup הוא user gesture תקף)
                        // התנהגות שונה במכוון מדאבל-קליק בעכבר.
                        toggleFullscreen();
                    } else {
                        // דאבל-קליק בעכבר → איפוס חירום
                        emergencyReset();
                    }
                } else {
                    clickTimer = setTimeout(() => {
                        clickTimer = null;
                        if (isMenuOpen()) closeMenu();
                        else openMenu();
                    }, DBL_WINDOW_MS);
                }
            };
    
            btn.addEventListener('pointerup', endPointer);
            btn.addEventListener('pointercancel', (e) => {
                if (drag && e.pointerId === drag.pointerId) {
                    clearTimeout(longPressTimer);
                    btn.classList.remove('dragging');
                    // אם הגרירה נקטעה אחרי הזזה בפועל — שומרים את המיקום שהושג
                    if (drag.canDrag && drag.dragged) savePosition();
                    drag = null;
                }
            });
        }
    
        // ------------------------------------------------------------------ ערוצי חירום
    
        function emergencyReset() {
            closeMenu();
            // איפוס + שמירה: normal נשמר כהעדפת הדומיין (התנהגות "כפתור חירום")
            applyMode('normal', { save: true });
        }
    
        function toggleFullscreen() {
            // עובד רק מתוך user gesture ישיר — ראו מגבלה #2 בראש הקובץ
            if (document.fullscreenElement) {
                document.exitFullscreen().catch(() => { /* ignore */ });
            } else {
                document.documentElement.requestFullscreen().catch(() => { /* ignore */ });
            }
        }
    
        // v4.3: איפוס רק ב-Esc כפול. Esc בודד נלחץ כל הזמן באתרים (יציאה ממסך
        // מלא ביוטיוב, סגירת חיפוש) והיה דורס את ההעדפה השמורה של המשתמש.
        let lastEscTime = 0;
        function onKeydown(e) {
            if (e.key !== 'Escape') return;
            if (isMenuOpen()) { closeMenu(); return; }
            const now = Date.now();
            if (now - lastEscTime < ESC_WINDOW_MS) {
                lastEscTime = 0;
                emergencyReset();
            } else {
                lastEscTime = now;
            }
        }
    
        // ------------------------------------------------------------------ אתחול
    
        function init() {
            if (!document.body) return;   // ביטחון — document-idle אמור להבטיח body
    
            currentMode = GM_getValue(location.hostname, 'normal');
            buildUI();
            clampHostPosition();
    
            targetHeaders = findAllHeaders();
            applyMode(currentMode, { save: false });
    
            startObserver();
            window.addEventListener('scroll', onScroll, { passive: true });
            window.addEventListener('resize', onResize);
            document.addEventListener('keydown', onKeydown, true);
    
            // ערוץ חירום רביעי — עובד גם אם הכפתור הצף נשבר/נעלם
            if (typeof GM_registerMenuCommand === 'function') {
                GM_registerMenuCommand('איפוס Header Manager באתר זה', emergencyReset);
            }
        }
    
        init();
    
        // API לדיבוג/בדיקות (לא חלק מהממשק למשתמש). נחשף על unsafeWindow כדי שאפשר
        // יהיה לגשת אליו מהקונסול הרגיל של הדפדפן (מחוץ ל-sandbox של Tampermonkey).
        const PAGE = (typeof unsafeWindow === 'object' && unsafeWindow) ? unsafeWindow : window;
        try {
            Object.defineProperty(PAGE, '__headerManagerDebug', {
                value: {
                    findAllHeaders,
                    rescanNow,
                    applyMode,
                    setAutoState,
                    runDeepScan,
                    getState: () => ({
                        currentMode, autoState,
                        targets: targetHeaders.slice(),
                        hideDistances: new Map(hideDistances),
                        scaleFactors: new Map(scaleFactors),
                    }),
                },
                configurable: true,
            });
        } catch (e) { /* ignore */ }
    })();
    
    תגובה 1 תגובה אחרונה
    0

    שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.

    נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.

    בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗

    הרשמה התחברות

    • התחברות

    • אין לך חשבון עדיין? הרשמה

    • התחברו או הירשמו כדי לחפש.
    • פוסט ראשון
      פוסט אחרון
    0
    • חוקי הפורום
    • פופולרי
    • לא נפתר
    • משתמשים
    • חיפוש גוגל בפורום
    • צור קשר