דילוג לתוכן

תוכנות להורדה

2.4k נושאים 37.5k פוסטים

תוכנות ממשתמשי הפורום, לא מיועד לשאלות, זה מיועד אך ורק להעלאות.

  • אינדקס | כל התוכנות והמדריכים שלי בקטגוריה אחת

    נעוץ נעול kingyos
    1
    156 הצבעות
    1 פוסטים
    28k צפיות
    אין תגובות
  • המלצה | אינדקס התוכנות החינמיות הטובות ביותר

    נעוץ
    72
    57 הצבעות
    72 פוסטים
    26k צפיות
    מ
    @נתן-מרדכי-שלום כתב בהמלצה | אינדקס התוכנות החינמיות הטובות ביותר: *PDF, עריכת עמודים ניתן לפצל למזג לחלץ ולסדר עמודים מחדש, ניתן גם לסובב עמודים, לגמרי בעברית, יש להתקין באנגלית ונהפך אוטומטי לעברית PDFSAM. איך זה אמור להפוך לעברית ? עדכנתי ועולה רק באנגלית.
  • להורדה | אינדקס תוכנות תורניות

    נעוץ
    246
    84 הצבעות
    246 פוסטים
    453k צפיות
    ב
    @האדם-החושב תודה
  • מדריך | פריצות לתוכנות - שרשור

    נעוץ
    459
    3
    44 הצבעות
    459 פוסטים
    166k צפיות
    מ
    @דאבל הוא נותן לך גם שם וקוד רשיון להכניס בפנים שזה מתוך הסריפקט שהוא מריץ
  • בקשה | תוכנת גיבוי GoodSync - מישהו יכול להעלות?

    20
    0 הצבעות
    20 פוסטים
    129 צפיות
    ש
    @דוכי אתה בטוח שהדיפנדר נשאר כבוי? יש פה תוכנה שמשביתה אותו לחלוטין
  • 25 הצבעות
    33 פוסטים
    350 צפיות
    אבייא
    @לאצי כתב: @אביי אשמח מאוד אם תעשה גרסת דפדפן ללא צורך בהתקנה. תודה רבה! אודות ל @לא-מתייאש שכתב את זה, עלה כעת גרסה חדשה שכוללת אתר https://abaye123.github.io/MusicRadio/live
  • להורדה | לוח דיגיטלי אופליין לבתי כנסיות!!

    29
    3
    16 הצבעות
    29 פוסטים
    669 צפיות
    נ
    @א.ב.ק. אני שמח לשמוע. יש עיר שלא מופיע ברשימה?
  • 92 הצבעות
    1k פוסטים
    93k צפיות
    מ
    היתה תקלה עולמית בשרתי גיטהאב. אולי קשור?
  • להורדה | IDM v6.43 Build 9 מנהל ומאיץ ההורדות הטוב ביותר!!!

    84
    2
    16 הצבעות
    84 פוסטים
    2k צפיות
    טופ שבמתמחיםט
    שימו לב מכיוון שהשרשור מתעדכן אז מומלץ לעקוב אחרי הנושא: [image: 1787003701601-496aacc6-5d42-4141-ab05-b35b17789eb8-image.jpeg]
  • מעכשיו אתם הופכים חחח ל-🤣 בגוגל-צ'אט במהירות!

    15
    1
    21 הצבעות
    15 פוסטים
    376 צפיות
    טופ שבמתמחיםט
    @אוריי אבל זה הקטע של הסקריפט שהוא להקלדה מהירה
  • להורדה | התוכנה החדשה לניהול גמ"ח כספים [גרסה 4.4.12]

    100
    1
    21 הצבעות
    100 פוסטים
    6k צפיות
    א
    @שליו תודה על הסדר הנפלא ששורר בגמ"ח שלנו בזכותך! אשריך שאתה עושה את זה בעין טובה!
  • 39 הצבעות
    28 פוסטים
    1k צפיות
    ה
    @אדיר-כהן הקובץ HTML לאחר שמתחילים לכתוב בו יוצר מסד נתונים שם נמצא כל התוכן אם מעתיקים את הקובץ HTML עם המסד לחדר מחשבים יהיה אפשר למחוק שם
  • להורדה | ניהול כספים

    141
    16 הצבעות
    141 פוסטים
    3k צפיות
    ש
    סתם שואל למה אין את האפשרות לטעון קובץ אקסל כמו שהיה בגרסא הראשונה? (בלי כל הסיבוך של כספיון)
  • 7 הצבעות
    8 פוסטים
    184 צפיות
    מ
    הקישור עודכן למעלה, נוספו המון פונקציות ואפשרויות כמו"כ הועלו צילומי מסך של התוכנה לפוסט הראשי, בספוילר. בהצלחה
  • 6 הצבעות
    38 פוסטים
    299 צפיות
    ד
    @אברהם-גלסר דבר ראשון, התוסף שלך מטורף. שנים חיכיתי למשהו שעובד באמת באתרוג ונטפרי, יישר כוח. בניתי את התוכנה כי חבר ביקש שאוריד לו ערוץ שלם, והשתגעתי להוריד קובץ-קובץ. לא מצאתי כלי שעושה את זה, אז בניתי לעצמי ואחר כך החלטתי לשתף. זה לא במקום התוסף שלך — זה משלים אותו. התוסף מושלם לסרטון אחד מכרום. התוכנה בשביל ערוץ, פלייליסט, ותור של קישורים שאפשר להשאיר לרוץ. בקיצור: הורדה המונית, ממשק בעברית עם מתקין, בלי צורך בתוכנות חיצוניות כמו ffmpeg.
  • להורדה | הורדה ישירות מיוטיוב!

    481
    1
    63 הצבעות
    481 פוסטים
    39k צפיות
    ר
    @דוכי צריך להוריד את הכוכביות מהקישור
  • 83 הצבעות
    380 פוסטים
    32k צפיות
    נ
    @ישראל-קרביצקי על תלחץ על טעינת תוסף פשוט תגרור את הקובץ זיפ לאיפה שכל התוספים כשאתה על מצב פיתוח
  • 5 הצבעות
    17 פוסטים
    197 צפיות
    מייבין במקצתמ
    @רוטב-שום יפה מאוד! למה לא בתור אתר? נראה לי שזה יהיה יותר נוח...
  • להורדה | תוסף לגלילה רציפה באתר פרוג קדימה ואחורה

    5
    1
    1 הצבעות
    5 פוסטים
    62 צפיות
    ל
    לחובבי הסקריפטים המרתי // ==UserScript== // @name גלילה רציפה - פרוג ועוד // @namespace https://freebuff.local/glila-rezifa // @version 1.0.0 // @description גרסת סקריפט (Tampermonkey) לתוסף "גלילה רציפה": קריאת עמודים ברצף בגלילה — טוען אוטומטית את העמוד הבא/הקודם בלי ללחוץ. תומך בפרוג ובאתרים נוספים. // @author גרסת סקריפט המבוססת על התוסף המקורי // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_prompt // @run-at document-idle // @noframes // ==/UserScript== // ============================================================================ // גלילה רציפה — גרסת סקריפט (Tampermonkey / Violentmonkey / Greasemonkey) // המרה מלאה מתוסף Chrome "גלילה רציפה - פרוג ועוד" (manifest v3). // // מה הוחלף מול התוסף: // * chrome.storage.sync → GM_setValue / GM_getValue // * הפופאפ ועמוד ההגדרות → תפריט הסקריפט (GM_registerMenuCommand): // - מצב הגלילה: קדימה+אחורה / רק קדימה / רק אחורה / כבוי // - הוספת אתר / הסרת האתר הנוכחי / איפוס הגדרות // * chrome.runtime.onMessage → פקודות תפריט ישירות (ללא רענון) // * content.css → GM_addStyle // * extsync-bridge (עדכונים אוטומטיים) → לא רלוונטי לסקריפט // מנוע הגלילה עצמו (ContinuousReader) נותר זהה לחלוטין לתוסף. // ============================================================================ (function () { "use strict"; // אם הסקריפט כבר רץ בעמוד הזה - לא מפעילים שוב. if (window.__continuousReaderLoaded) return; window.__continuousReaderLoaded = true; // --------------------------------------------------------------------------- // סטיילים (הועתקו מ-content.css של התוסף) // --------------------------------------------------------------------------- GM_addStyle(` /* מפריד בין עמודים שנטענו ברצף — עיצוב ברור: קו + תגית עמוד באמצע */ .cr-page-separator { display: flex; align-items: center; gap: 12px; margin: 30px 0; font-family: system-ui, "Segoe UI", Arial, sans-serif; user-select: none; direction: rtl; } .cr-page-separator::before, .cr-page-separator::after { content: ""; flex: 1 1 auto; height: 2px; background: linear-gradient(to left, transparent, #c7cdf0); } .cr-page-separator::after { background: linear-gradient(to right, transparent, #c7cdf0); } /* כשגוללים לתחילת עמוד חדש שנטען, רוצים שהתגית תיראה מיד למעלה בלי מרווח גדול מעליה. */ .cr-page-section > .cr-page-separator:first-child { margin-top: 4px; } /* בטעינה אחורה התגית נמצאתית ה-section (בתפר מול התוכן הישן) — רוצים שהיא תיראה קרוב לתחתית המסך בלי מרווח גדול מתחתיה. */ .cr-page-section > .cr-page-separator:last-child { margin-bottom: 4px; } .cr-page-badge { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; background: #4a5bdc; color: #fff; font-size: 13px; font-weight: 700; letter-spacing: 0.2px; padding: 6px 16px; border-radius: 999px; box-shadow: 0 3px 10px rgba(74, 91, 220, 0.35); white-space: nowrap; } .cr-page-badge .cr-page-badge-icon { font-size: 13px; line-height: 1; } .cr-page-section { /* אנימציית הופעה עדינה */ animation: cr-fade-in 0.35s ease; } @keyframes cr-fade-in { from { opacity: 0; } to { opacity: 1; } } /* חיווי צף — מוצג לזמן קצר בלבד בזמן פעולה, ונעלם לבד כדי לא להפריע לקריאה כשגוללים עמוק בתוך התוכן. */ .cr-indicator { position: fixed; bottom: 18px; left: 18px; z-index: 2147483647; background: rgba(30, 34, 45, 0.92); color: #fff; font-family: system-ui, "Segoe UI", Arial, sans-serif; font-size: 12px; padding: 7px 12px; border-radius: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25); direction: rtl; pointer-events: none; opacity: 0; transform: translateY(6px); transition: opacity 0.25s ease, transform 0.25s ease; } .cr-indicator-show { opacity: 0.92; transform: translateY(0); } `); // --------------------------------------------------------------------------- // שכבת התאמה לסקריפט — במקום chrome.storage.sync // --------------------------------------------------------------------------- // אחסון פשוט על גבי GM_setValue/GM_getValue (ערכים כקובצי JSON). const store = { async get(keys) { const out = {}; for (const k of keys) { const raw = GM_getValue("cr_" + k, undefined); if (raw === undefined) continue; try { out[k] = JSON.parse(raw); } catch (e) { out[k] = raw; // ערך ישן/לא-JSON — מחזירים כמו שהוא. } } return out; }, async set(obj) { for (const [k, v] of Object.entries(obj)) { GM_setValue("cr_" + k, JSON.stringify(v)); } } }; // תיבת קלט ידידותית לסקריפט (GM_prompt), עם גיבוי ל-prompt רגיל. function gmPrompt(message, def) { return new Promise((resolve) => { try { const r = GM_prompt(message, def || ""); if (r && typeof r.then === "function") { r.then(resolve); return; } resolve(r); } catch (e) { resolve(window.prompt(message, def || "")); } }); } // הגדרות ברירת מחדל (מהתוסף — background.js). const DEFAULT_SETTINGS = { // כיוון הגלילה הרציפה: // "both" = קדימה ואחורה // "forward" = רק קדימה (טעינת העמוד הבא) // "backward" = רק אחורה (טעינת העמוד הקודם) // "off" = כבוי direction: "both", thresholdForward: 1200, // כמה פיקסלים לפני סוף העמוד להתחיל לטעון קדימה thresholdBackward: 800, // כמה פיקסלים מראש העמוד להתחיל לטעון אחורה showIndicator: true // הצגת חיווי צף על המסך }; // פרוג בנוי על מנוע פורומים XenForo, ולכן משתמשים במצב הייעודי "xenforo" // עם סלקטורים מדויקים — כך הטעינה מהירה ומדויקת. // הערה: content.js אוכף שמצב "xenforo" פועל רק בעמודי נושא אמיתיים // (כתובת עם "/threads/"), כדי לא לרוץ בטעות על עמודי רשימת פורום/דף // הבית שמלאים במודעות ותצוגות מקדימות. function progSite() { return { id: "prog", name: "פרוג", match: "prog.co.il", // נבדק מול שם הדומיין mode: "xenforo", // "xenforo" | "auto" | "manual" contentSelector: "", nextSelector: "", prevSelector: "", enabled: true }; } // אתחול/מיגרציה: מבטיח שקיימות הגדרות, ושפרוג מוגדר במצב xenforo. async function ensureDefaults() { const stored = await store.get(["settings", "sites"]); const toSet = {}; if (!stored.settings) { toSet.settings = DEFAULT_SETTINGS; } let sites = stored.sites; if (!sites || !sites.length) { sites = [progSite()]; toSet.sites = sites; } else { // מיגרציה: אם פרוג קיים אך לא במצב xenforo — מתקנים אותו. let changed = false; let hasProg = false; sites = sites.map((s) => { if ((s.match || "").toLowerCase().includes("prog")) { hasProg = true; if (s.mode !== "xenforo") { changed = true; return Object.assign({}, s, { mode: "xenforo" }); } } return s; }); if (!hasProg) { sites = [progSite(), ...sites]; changed = true; } if (changed) toSet.sites = sites; } if (Object.keys(toSet).length) { await store.set(toSet); } } // --------------------------------------------------------------------------- // המנוע (הועתק מ-content.js של התוסף, עם התאמות הסקריפט בלבד) // --------------------------------------------------------------------------- // מילות מפתח לזיהוי אוטומטי של קישורי "הבא" ו"הקודם" (עברית + אנגלית). const NEXT_WORDS = [ "עמוד הבא", "לעמוד הבא", "הדף הבא", "לדף הבא", "העמוד הבא", "הבא", "קדימה", "next", "older", "ישן יותר", "המשך", "›", "»", "→", ">>" ]; const PREV_WORDS = [ "עמוד קודם", "לעמוד הקודם", "הדף הקודם", "לדף הקודם", "העמוד הקודם", "הקודם", "אחורה", "prev", "previous", "newer", "חדש יותר", "‹", "«", "←", "<<" ]; // --------------------------------------------------------------------------- // שירותי עזר // --------------------------------------------------------------------------- function scroller() { return document.scrollingElement || document.documentElement; } // המרת URL יחסי לכתובת מלאה מול בסיס נתון. function absolutize(url, base) { try { return new URL(url, base).href; } catch (e) { return null; } } // בדיקה אם טקסט הקישור מכיל אחת ממילות המפתח. function matchesWords(text, words) { const t = (text || "").trim().toLowerCase(); if (!t) return false; return words.some((w) => t.includes(w.toLowerCase())); } // חיפוש קישור לפי מילות מפתח בתוך מסמך. function findLinkByWords(doc, words) { const anchors = Array.from(doc.querySelectorAll("a[href]")); for (const a of anchors) { const label = (a.textContent || "") + " " + (a.getAttribute("title") || "") + " " + (a.getAttribute("aria-label") || ""); if (matchesWords(label, words)) return a.getAttribute("href"); } return null; } // ניסיון להסיק את כתובת העמוד הבא/הקודם מתוך מספר עמוד בכתובת ה-URL. // מזהה תבניות כמו page=3, /page/3, p=3, /3 בסוף הנתיב. function urlByPageDelta(urlStr, delta) { let url; try { url = new URL(urlStr); } catch (e) { return null; } // 1) פרמטרים נפוצים ב-query string const params = ["page", "p", "pg", "paged"]; for (const key of params) { if (url.searchParams.has(key)) { const n = parseInt(url.searchParams.get(key), 10); if (!isNaN(n)) { const next = n + delta; if (next < 1) return null; url.searchParams.set(key, String(next)); return url.href; } } } // 2) תבנית /page/3 או /page-3 בנתיב (XenForo משתמש ב-/page-3) let m = url.pathname.match(/(\/page[\/-])(\d+)/i); if (m) { const next = parseInt(m[2], 10) + delta; if (next < 1) return null; url.pathname = url.pathname.replace(m[0], m[1] + next); return url.href; } // הערה: בכוונה איננו מנחשים "מספר כלשהו בסוף הכתובת", כי לרוב זהו // מזהה נושא/מאמר ולא מספר עמוד — ניחוש כזה טוען עמודים לא קיימים. return null; } // --------------------------------------------------------------------------- // זיהוי אלמנט התוכן וכתובות הבא/הקודם (לפי מצב auto או manual) // --------------------------------------------------------------------------- // סלקטורים מדויקים למבנה עמוד "נושא" (Thread) בפרוג — מנוע XenForo 2. // הסקריפט פועל *רק* על עמודי נושא (כתובת שמכילה "/threads/"), ולא על // עמודי רשימת פורום/דף הבית — שם יש הרבה מודעות ותצוגות מקדימות // שאינן פוסטים אמיתיים (ראו בדיקת המסלול ב-boot()). const XF = { // מיכל ההודעות בעמוד נושא. ההודעות עצמן (<article class="message ...">) // הן ילדים ישירים שלו, וזה מה שמאפשר לסנן החוצה מודעות/ווידג'טים // שמוזרקים בין ההודעות ואינם <article class="message">. threadContent: ".block-body.js-replyNewMessageContainer", // כל הודעה בודדת (פוסט) בתוך הנושא. post: "article.message", // ניווט עמודים של פרוג (XenForo pageNav). next: "a.pageNav-jump--next", prev: "a.pageNav-jump--prev", pageCurrent: ".pageNav-page--current", pageLinks: ".pageNav-page a" }; // בדיקה אם כתובת נתונה היא עמוד "נושא" אמיתי בפרוג (ולא רשימת פורום/דף הבית). function isProgThreadUrl(urlStr) { try { return /\/threads\//i.test(new URL(urlStr).pathname); } catch (e) { return false; } } // חילוץ מספר העמוד האמיתי (ומספר העמודים הכולל, אם ידוע) מתוך עמוד XenForo. // מבוסס על סימון העמוד הנוכחי בניווט העמודים (pageNav), ולא על מונה פנימי — // כך שהמספר שמוצג תמיד תואם את מספר העמוד האמיתי באתר. function getXfPageInfo(doc, urlStr) { let page = NaN; const curEl = doc.querySelector(XF.pageCurrent); if (curEl) page = parseInt((curEl.textContent || "").trim(), 10); if (isNaN(page)) { const m = (urlStr || "").match(/\/page-(\d+)/i); page = m ? parseInt(m[1], 10) : 1; // בפרוג, לעמוד הראשון אין סיומת page- } let total = 0; doc.querySelectorAll(XF.pageLinks).forEach((a) => { const n = parseInt((a.textContent || "").trim(), 10); if (!isNaN(n) && n > total) total = n; }); if (!total || total < page) total = page; return { page, total }; } // שליפת רשימת ההודעות (הפוסטים) בלבד מעמוד נושא, בלי מודעות/ווידג'טים // שמוזרקים בין ההודעות (למשל "נושאים דומים", מודעות בין-הודעות וכו'). function getXfPostNodes(doc) { const container = doc.querySelector(XF.threadContent); if (!container) return null; let posts = Array.from(container.children).filter((el) => el.tagName === "ARTICLE" && el.classList.contains("message") ); if (!posts.length) { // חלופה זהירה למקרה של תבנית עם עטיפה נוספת סביב הפוסטים. posts = Array.from(container.querySelectorAll(XF.post)); } return posts.length ? posts : null; } // בחירת האלמנט שמכיל את התוכן הראשי בעמוד. function getContentElement(doc, cfg) { // מצב XenForo: מיכל ההודעות של עמוד הנושא בלבד (לא עמודי רשימה/מודעות). if (cfg.mode === "xenforo") { return doc.querySelector(XF.threadContent) || null; } if (cfg.mode === "manual" && cfg.contentSelector) { return doc.querySelector(cfg.contentSelector); } // זיהוי אוטומטי לפי סלקטורים נפוצים. const candidates = [ "main", "article", "[role='main']", "#content", ".content", "#main", ".main", "#post", ".post", ".entry-content", ".article-body", ".page-content", "#primary" ]; for (const sel of candidates) { const el = doc.querySelector(sel); if (el && el.textContent && el.textContent.trim().length > 80) return el; } // fallback: הבלוק עם הכי הרבה טקסט מבין ילדי ה-body. let best = null, bestLen = 0; const children = doc.body ? Array.from(doc.body.children) : []; for (const c of children) { const tag = c.tagName.toLowerCase(); if (["header", "footer", "nav", "script", "style", "aside"].includes(tag)) continue; const len = (c.textContent || "").trim().length; if (len > bestLen) { bestLen = len; best = c; } } return best || doc.body; } // קבלת כתובת העמוד הבא מתוך מסמך. function getNextUrl(doc, baseUrl, cfg) { let href = null; if (cfg.mode === "xenforo") { const el = doc.querySelector(XF.next) || doc.querySelector('a[rel~="next"]') || doc.querySelector('link[rel~="next"]'); href = el ? el.getAttribute("href") : null; if (!href) href = findLinkByWords(doc, NEXT_WORDS); } else if (cfg.mode === "manual" && cfg.nextSelector) { const el = doc.querySelector(cfg.nextSelector); if (el) href = el.getAttribute("href") || el.dataset.href; } else { const rel = doc.querySelector('a[rel~="next"], link[rel~="next"]'); if (rel) href = rel.getAttribute("href"); if (!href) href = findLinkByWords(doc, NEXT_WORDS); if (!href) href = urlByPageDelta(baseUrl, +1); // ייגזר יחסית לבסיס למטה } if (!href) return null; // אם href כבר מלא נשאיר, אחרת נהפוך למלא מול baseUrl if (/^https?:\/\//i.test(href)) return href; return absolutize(href, baseUrl); } // קבלת כתובת העמוד הקודם מתוך מסמך. function getPrevUrl(doc, baseUrl, cfg) { let href = null; if (cfg.mode === "xenforo") { const el = doc.querySelector(XF.prev) || doc.querySelector('a[rel~="prev"]') || doc.querySelector('link[rel~="prev"]'); href = el ? el.getAttribute("href") : null; } else if (cfg.mode === "manual" && cfg.prevSelector) { const el = doc.querySelector(cfg.prevSelector); if (el) href = el.getAttribute("href") || el.dataset.href; } else { const rel = doc.querySelector('a[rel~="prev"], link[rel~="prev"]'); if (rel) href = rel.getAttribute("href"); if (!href) href = findLinkByWords(doc, PREV_WORDS); if (!href) href = urlByPageDelta(baseUrl, -1); } if (!href) return null; if (/^https?:\/\//i.test(href)) return href; return absolutize(href, baseUrl); } // --------------------------------------------------------------------------- // המנוע הראשי // --------------------------------------------------------------------------- class ContinuousReader { constructor(cfg, settings) { this.cfg = cfg; this.settings = settings; this.direction = settings.direction || "both"; this.contentEl = null; // אלמנט התוכן המקורי this.firstEl = null; // הבלוק העליון ביותר בשרשרת (לטעינה אחורה) this.lastEl = null; // הבלוקון ביותר בשרשרת (לטעינה קדימה) this.lastMessageEl = null; // ההודעה האחרונה בפועל בעמוד הנוכחי (לטריגר מדויק) this.firstMessageEl = null; // ההודעה הראשונה בפועל בעמוד הנוכחי (לטריגר מדויק אחורה) this.forwardUrl = null; this.backwardUrl = null; this.visited = new Set([location.href]); this.loadingF = false; this.loadingB = false; // טיימרים של מעבר עמוד מתוזמן (ראו _scheduleLoad) — null כשאין מעבר ממתין. this._forwardLoadTimer = null; this._backwardLoadTimer = null; // מספרי העמוד המוצגים תמיד נלקחים מתוך העמוד עצמו (pageNav של פרוג), // ולא ממונה מקומי — כדי שהתווית תציג את מספר העמוד האמיתי באתר. this.pageForward = 1; this.pageBackward = 1; this.pageTotal = 0; this.indicator = null; // עוטף כדי שאירוע הגלילה לא יעביר את ה-Event כ-forwardOnly. this._scrollHandler = () => this._onScroll(false); // רשימת כל העמודים שנטעלו עד כה (כולל העמוד המקורי), מסודרת לפי // סדר הופעתם במסמך (מלמעלה למטה = ממספר עמוד קטן לגדול). כל איבר: // { page, url, el }. משמשת כדי לדעת בכל רגע איזה עמוד "בתצוגה" // ולעדכן את שורת הכתובת בהתאם (ראו _updateAddressBar). this.sections = []; this._lastUrlSynced = location.href; // "טעינה מוקדמת" (prefetch): מביאים את העמוד הבא/הקודם ברקע עוד // לפני שמגיעים לסף הגלילה, כך שברגע שצריך להציג אותו זה כמעט מיידי — // בקצב הגלילה עצמה, בלי המתנה לרשת. this._prefetchCache = { forward: null, backward: null }; // כל בקשות ה-fetch שכרגע "באוויר" (כולל טעינות מוקדמות ברקע) - // כדי שנוכל לבטל אותן בצורה נקייה אם הדף ננטש (ראו _abortAllFetches). this._activeFetchControllers = new Set(); } async init() { this.contentEl = getContentElement(document, this.cfg); if (!this.contentEl || !this.contentEl.parentNode) { console.warn("[גלילה רציפה] לא נמצא אלמנט תוכן מתאים בעמוד."); return; } this.firstEl = this.contentEl; this.lastMessageEl = this._lastMessageIn(this.contentEl); this.firstMessageEl = this._firstMessageIn(this.contentEl); // עוגן ההוספה לטעינה קדימה: אם יש הודעה אחרונה מזוהה בתוך מיכל // התוכן (כמו בפרוג/XenForo), מוסיפים את העמוד הבא ישר אחריה — ולא // אחרי כל מיכל התוכן, שעלול לכלול גם תיבת תגובה/ווידג'טים אחרי // ההודעה האחרונה. בלי זה, בטעינה הקדימה הראשונה התגית עם מספר // העמוד "נקברת" הרחק אחרי כל אותו תוכן נוסף ולא נראית כשמגיעים // לסוף ההודעות (למרות שהטריגר נבדק לפי ההודעה האחרונה עצמה). this.lastEl = (this.lastMessageEl && this.lastMessageEl.parentNode) ? this.lastMessageEl : this.contentEl; this.forwardUrl = getNextUrl(document, location.href, this.cfg); this.backwardUrl = getPrevUrl(document, location.href, this.cfg); if (this.cfg.mode === "xenforo") { const info = getXfPageInfo(document, location.href); this.pageForward = info.page; this.pageBackward = info.page; this.pageTotal = info.total; } // רישום העמוד המקורי (זה שממנו האתחול קרה) ברשימת העמודים שנטעלו. this.sections.push({ page: this.cfg.mode === "xenforo" ? this.pageForward : 1, url: location.href, el: this.contentEl }); console.log("[גלילה רציפה] אותחל.", "מצב:", this.cfg.mode, "| תוכן:", this.contentEl && (this.contentEl.className || this.contentEl.tagName), "| עמוד הבא:", this.forwardUrl || "לא נמצא", "| עמוד קודם:", this.backwardUrl || "לא נמצא", "| כיוון:", this.direction); if (!this.forwardUrl && !this.backwardUrl) { console.warn("[גלילה רציפה] לא נמצאו קישורי הבא/קודם בעמוד הזה — אין מה לטעון."); } if (this.settings.showIndicator) this._createIndicator(); this._updateIndicator("מוכן"); // התחלת טעינה מוקדמת ברקע כבר מהאתחול. this._prefetch("forward"); this._prefetch("backward"); window.addEventListener("scroll", this._scrollHandler, { passive: true }); // מבטלים בקשות רשת שעדיין באוויר (בעיקר טעינות מוקדמות ברקע) כשעוזבים // את הדף - כדי לא להשאיר Promise שנדחה על "Failed to fetch" סתם כי // הדף כבר לא קיים. pagehide נורה גם בניווט רגיל וגם בסגירת טאב. this._onPageHide = () => this._abortAllFetches(); window.addEventListener("pagehide", this._onPageHide); // בדיקה ראשונית (למקרה שהעמוד קצר מהמסך) — קדימה בלבד. setTimeout(() => this._onScroll(true), 300); } // מציאת ההודעה (הפוסט) האחרונה בפועל בתוך מיכל נתון — כדי לדעת בדיוק // מתי הקורא "הגיע" אליה, ולא לפי מרחק בפיקסלים מתחתית המיכל כולו // (שעלולה לכלול פוטר, תיבת תגובה, "נושאים דומים" וכו'). _lastMessageIn(container) { if (!container) return null; if (this.cfg.mode === "xenforo") { const posts = container.querySelectorAll(XF.post); if (posts.length) return posts[posts.length - 1]; } return container; } // מציאת ההודעה (הפוסט) הראשונה בפועל בתוך מיכל נתון — משמש לטריגר // המדויק של הטעינה אחורה (רק כשמגיעים בפועל להודעה הראשונה בעמוד). _firstMessageIn(container) { if (!container) return null; if (this.cfg.mode === "xenforo") { const posts = container.querySelectorAll(XF.post); if (posts.length) return posts[0]; } return container; } // "נועלים" זמנית גלילה ידנית (גלגלת עכבר / מגע) למשך אנימציית הנחיתה. // בלי זה, אם המשתמש ממשיך לגלול בפועל (למשל תנופה שנמשכת אחרי החלקה // בטראצפד) בזמן שה-JS מנסה לגלול לעמוד/לתחתית ה-section, שתי הגלילות // "נלחמות" זו בזו — ובגלילה קדימה (שבניגוד לאחורה לא נעצרת טבעית בקצה // המסמך) זה גורם לכך שהנחיתה כמעט ולא מורגשת. הנעילה משתחררת אוטומטית // ברגע שהאנימציה מסתיימת, כך שהיא לא פוגעת בגלילה רגילה בשאר הזמן. _lockNativeScroll() { this._scrollLockCount = (this._scrollLockCount || 0) + 1; if (this._scrollLockCount > 1) return; const prevent = (e) => e.preventDefault(); this._scrollLockPrevent = prevent; window.addEventListener("wheel", prevent, { passive: false }); window.addEventListener("touchmove", prevent, { passive: false }); } _unlockNativeScroll() { this._scrollLockCount = Math.max(0, (this._scrollLockCount || 1) - 1); if (this._scrollLockCount > 0) return; if (this._scrollLockPrevent) { window.removeEventListener("wheel", this._scrollLockPrevent); window.removeEventListener("touchmove", this._scrollLockPrevent); this._scrollLockPrevent = null; } } // גלילה עדינה אל יעד - אבל בניגוד לגרסה קודמת, היעד לא מחושב פעם אחת // מראש אלא *מחדש בכל פריים* (getTargetY היא פונקציה, לא מספר קבוע). // // למה זה חשוב: כשנוחתים על "ראש" עמוד חדש שנטען, אם בזמן האנימציה // (700 מ"ש) יש שינוי בגובה הדף - למשל תמונה/אווטאר שמשלימים טעינה // ודוחפים תוכן למטה, או תנופת גלילה טבעית (טראצקפד/מגע) שממשיכה // למרות הנעילה, או אפילו scroll-behavior:smooth שהאתר מגדיר - אז יעד // קבוע שחושב פעם אחת "מתיישן" עד שמגיעים אליו, וכתוצאה נוחתים במקום // הלא נכון (למשל עמוק בתוך הפוסט הבא, אחרי הכותרת שלו, במקום על // התגית). הפתרון: בכל פריים מחשבים מחדש איפה היעד *באמת* נמצא כרגע, // ומתקדמים חלק לכיוונו (כמו "מכ״ם רודף") - כך שגם אם התוכן זז תוך // כדי, מתכנסים למקום הנכון בפועל ולא למקום שהיה נכון שנייה קודם. _smoothScrollTo(getTargetY, duration) { const targetFn = typeof getTargetY === "function" ? getTargetY : () => getTargetY; const dur = duration || 700; const startTime = performance.now(); let settledFrames = 0; this._lockNativeScroll(); const step = (now) => { const t = (now - startTime) / dur; const currentY = window.scrollY || document.documentElement.scrollTop; const liveTarget = targetFn(); const gap = liveTarget - currentY; // מקדם ההתקדמות: מתחיל עדין (לא קפיצה), ומתחזק ככל שמתקדמים - // כך שגם אם היעד "זז" באמצע, מתכנסים אליו בפועל ולא נתקעים. const speed = Math.min(0.35, 0.06 + t * 0.5); let nextY; if (t >= 1 || Math.abs(gap) < 1.5) { nextY = liveTarget; // תמיד "נצמדים" בדיוק ליעד העדכני בסוף. } else { nextY = currentY + gap * speed; } // behavior: "auto" מפורש בכוונה, כדי לבטל scroll-behavior:smooth // שהאתר עצמו אולי מגדיר ב-CSS - אחרת קריאה "סתמית" הייתה מופעלת // כאנימציה חלקה נוספת של הדפדפן, שמצטברת עם הלולאה שלנו כאן // וגורמת להחמצת יעד (overshoot). window.scrollTo({ top: Math.max(0, nextY), left: 0, behavior: "auto" }); if (Math.abs(liveTarget - nextY) < 1.5) { settledFrames++; } else { settledFrames = 0; } // ממשיכים עד שממש מתייצבים על היעד העדכני (כמה פריימים רצוף), // לא רק עד שחלף הזמן - כך שאם היעד המשיך לזוז, ממשיכים לרדוף. if (settledFrames < 3 && (now - startTime) < dur + 900) { requestAnimationFrame(step); } else { this._unlockNativeScroll(); } }; requestAnimationFrame(step); } // ממתין שהגלילה "תירגע" (אין יותר שינוי מורגש במיקום הגלילה) לפני // שמפעילים קולבק - כדי לא להתחיל את אנימציית הנחיתה בדיוק באמצע תנופה // פעילה של המשתמש, מה שהיה גורם לשתי הגלילות "להיאבק" זו בזו. יש // תקרת זמן (maxWait) כדי שלא ניתקע לנצח אם הדפדפן מדווח על תזוזה זעירה // מתמשכת. _waitForScrollIdle(callback, maxWait, idleFrames) { const start = performance.now(); const needed = idleFrames || 4; const cap = maxWait || 1500; let lastY = window.scrollY || document.documentElement.scrollTop; let stable = 0; const check = () => { const y = window.scrollY || document.documentElement.scrollTop; if (Math.abs(y - lastY) < 0.5) { stable++; } else { stable = 0; lastY = y; } if (stable >= needed || (performance.now() - start) > cap) { callback(); return; } requestAnimationFrame(check); }; requestAnimationFrame(check); } // גלילה לראש ה-section החדש, כך שהתגית עם מספר העמוד תיראה מיד בראש // המסך — בעדינות ולא בקפיצה חדה. היעד מחושב מחדש בכל פריים (ראו // _smoothScrollTo) כדי להתמודד עם תוכן שזז תוך כדי (תמונות שנטענות // וכו'). _scrollToSectionTop(section) { this._smoothScrollTo(() => { const rect = section.getBoundingClientRect(); return Math.max(0, rect.top + (window.scrollY || document.documentElement.scrollTop) - 6); }, 700); } // גלילה לתחתית ה-section החדש (בשימוש בטעינה אחורה): "נוחתים" על סוף // העמוד שנטען, כך שהתגית עם מספר העמוד — שבטעינה אחורה נמצאתית // ה-section, בדיוק בנקודת המעבר לתוכן הישן — תיראה מיד למטה במסך. // גם כאן היעד מחושב מחדש בכל פריים. _scrollToSectionBottom(section) { this._smoothScrollTo(() => { const rect = section.getBoundingClientRect(); const currentY = window.scrollY || document.documentElement.scrollTop; const vh = window.innerHeight; return Math.max(0, rect.bottom + currentY - vh + 10); }, 700); } setDirection(dir) { this.direction = dir; this._updateIndicator(this._dirLabel()); if (dir === "off") { this._cancelScheduledLoad("forward"); this._cancelScheduledLoad("backward"); } // בדיקה מיידית אם צריך לטעון עכשיו. this._onScroll(); } _dirLabel() { switch (this.direction) { case "both": return "פעיל: קדימה + אחורה"; case "forward": return "פעיל: קדימה"; case "backward": return "פעיל: אחורה"; default: return "כבוי"; } } _forwardEnabled() { return this.direction === "both" || this.direction === "forward"; } _backwardEnabled() { return this.direction === "both" || this.direction === "backward"; } // forwardOnly=true בבדיקה הראשונית, כדי לא לטעון אחורה אוטומטית בעת הטעינה. _onScroll(forwardOnly) { if (this.direction === "off") return; const vh = window.innerHeight; // קדימה: הטריגר מבוסס על ההודעה האחרונה בפועל בעמוד — מגיעים אליה // רק ברגע שהיא נכנסת לתצוגה, לא לפני. הטעינה המוקדמת (prefetch) // כבר דואגת שהעמוד הבא יהיה מוכן ברקע. עם זאת, לא טוענים באופן // מיידי — ראו _scheduleLoad. if (this._forwardEnabled() && this.forwardUrl && !this.loadingF && this.lastMessageEl) { const rect = this.lastMessageEl.getBoundingClientRect(); if (rect.top <= vh) { this._scheduleLoad("forward"); } else { this._cancelScheduledLoad("forward"); } } else { this._cancelScheduledLoad("forward"); } // אחורה: לא לפי מרחק פיקסלים מראש העמוד, אלא בדיוק כמו קדימה — // רק ברגע שההודעה הראשונה בפועל בעמוד נכנסת לתצוגה (מגיעים אליה // ממש). כך הטעינה אחורה מתרחשת אך ורק כשמגיעים לתגובה הראשונה, // בדיוק כמו שהטעינה קדימה מתרחשת רק כשמגיעים לתגובה האחרונה. if (!forwardOnly && this._backwardEnabled() && this.backwardUrl && !this.loadingB && this.firstMessageEl) { const rect = this.firstMessageEl.getBoundingClientRect(); if (rect.bottom >= 0) { this._scheduleLoad("backward"); } else { this._cancelScheduledLoad("backward"); } } else if (!forwardOnly) { this._cancelScheduledLoad("backward"); } // עדכון שורת הכתובת לפי העמוד שבתצוגה כרגע. this._updateAddressBar(); } // מתזמן מעבר עמוד אוטומטי בעוד כשנייה וחצי, במקום לבצע אותו באופן // מיידי ברגע שמגיעים לסוף/תחילת העמוד. שתי סיבות לזה: // 1) נותן למשתמש חלון זמן להעביר עמוד *ידנית* בעצמו (למשל בלחיצה על // קישור "עמוד הבא" באתר) אם הוא מעדיף, במקום שהסקריפט "יחטוף" לו // את הגלילה. // 2) עד שהטיימר מסתיים, תנופת הגלילה של המשתמש (למשל גלילה בטראצקפד) // כבר נרגעה ונעצרה — כך שכשבאמת מתבצעת ההוספה והנחיתה על העמוד // החדש, אין גלילה ידנית מתחרה שמבטלת את התחושה של הנחיתה. // אם המשתמש מתרחק בינתיים מסף הטעינה (גולל בחזרה) — הטיימר מבוטל. _scheduleLoad(direction) { const key = direction === "forward" ? "_forwardLoadTimer" : "_backwardLoadTimer"; if (this[key]) return; // כבר מתוזמן this._updateIndicator( direction === "forward" ? "עובר לעמוד הבא בעוד רגע… (אפשר גם ידנית)" : "עובר לעמוד הקודם בעוד רגע… (אפשר גם ידנית)", 2200 ); this[key] = setTimeout(() => { this[key] = null; if (direction === "forward") this._loadNext(); else this._loadPrev(); }, 2000); } _cancelScheduledLoad(direction) { const key = direction === "forward" ? "_forwardLoadTimer" : "_backwardLoadTimer"; if (this[key]) { clearTimeout(this[key]); this[key] = null; } } // מביא ומפרסר מסמך מכתובת נתונה. משתמש ב-AbortController כדי שאם // הדף ננטש (המשתמש עוזב/סוגר את הטאב) תוך כדי טעינה מוקדמת ברקע, // הבקשה תבוטל בצורה נקייה במקום להיכשל עם "Failed to fetch". async _fetchDoc(url) { const controller = new AbortController(); this._activeFetchControllers.add(controller); try { const res = await fetch(url, { credentials: "include", signal: controller.signal }); if (!res.ok) throw new Error("HTTP " + res.status); const html = await res.text(); const doc = new DOMParser().parseFromString(html, "text/html"); return doc; } finally { this._activeFetchControllers.delete(controller); } } // מבטל את כל הבקשות שעדיין באוויר (למשל טעינות מוקדמות ברקע). נקרא // כשהדף ננטש, כדי לא להשאיר בקשות תלויות שנכשלות אחרי שכבר לא רלוונטי. _abortAllFetches() { this._activeFetchControllers.forEach((c) => { try { c.abort(); } catch (e) { /* מתעלמים */ } }); this._activeFetchControllers.clear(); } // מתחיל הבאה ברקע (ללא await) של עמוד ה-URL הנוכחי בכיוון המבוקש, // ושומר את ה-Promise במטמון קטן לפי כיוון. כך כשמגיעים לסף הגלילה // בפועל, הטעינה כבר כמעט הושלמה ותוצג כמעט מיידית. _prefetch(direction) { const url = direction === "forward" ? this.forwardUrl : this.backwardUrl; if (!url) return; const cache = this._prefetchCache[direction]; if (cache && cache.url === url) return; // כבר בטעינה/הובא בעבר עבור אותו URL const promise = this._fetchDoc(url).catch((e) => { // אם נכשל, מנקים את המטמון כדי שניסיון אמיתי בהמשך יחזור לנסות ברשת ישירות. if (this._prefetchCache[direction] && this._prefetchCache[direction].url === url) { this._prefetchCache[direction] = null; } throw e; }); // חשוב: מוסיפים כאן ענף-צל שקט שתופס דחייה (reject) גם אם בסוף אף // אחד לא צורך את ה-Promise הזה בפועל (למשל אם המשתמש עוזב את הדף, // מכבה את הכיוון, או שהטעינה המוקדמת נכשלת בלי שמגיעים לסף // שמפעיל _getDoc). בלי זה, הדפדפן מדווח "Uncaught (in promise)" // בקונסולה על טעינות רקע כושלות שבכלל לא נחוצות היו בסוף - זה לא // באמת באג פונקציונלי, רק "רעש" בלוג. הענף המקורי (cache.promise) // ממשיך להידחות כרגיל עבור מי שבאמת מחכה לו ב-_getDoc. promise.catch(() => {}); this._prefetchCache[direction] = { url, promise }; } // מחזיר את המסמך של ה-URL המבוקש: מהמטמון אם כבר מוכן/בדרך, אחרת מביא ישירות. async _getDoc(direction, url) { const cache = this._prefetchCache[direction]; if (cache && cache.url === url) { this._prefetchCache[direction] = null; try { return await cache.promise; } catch (e) { // נופל חזרה לניסיון ישיר אם הטעינה המוקדמת נכשלה. } } return this._fetchDoc(url); } // סלקטורים למודעות/ווידג'טים שעלולים להישאר בתוך הודעה בודדת (הגנה נוספת, // מעבר לכך שכבר בוחרים רק <article class="message"> ולא אלמנטים אחרים). static get AD_SELECTORS() { return "ins.adsbygoogle, iframe[src*='ads'], iframe[src*='doubleclick'], " + "[class*='adUnit' i], [id*='google_ads' i], .p-body-pageContent-ad, .js-ad"; } // בניית section חדש מתוך רשימת הודעות (posts) שהובאו מעמוד אחר. // sourceNodes: מערך של אלמנטי <article class="message"> בלבד (ללא מודעות), // או, במצב auto/manual, מערך עם אלמנט תוכן בודד. _buildSection(sourceNodes, baseUrl, pageInfo, dir) { const wrapper = document.createElement("div"); wrapper.className = "cr-page-section"; wrapper.setAttribute("data-cr-page", pageInfo.page); // מחוון מפריד בין עמודים — עיצוב ברור (תגית + קו), מציג את מספר // העמוד האמיתי באתר. const sep = document.createElement("div"); sep.className = "cr-page-separator"; const badge = document.createElement("span"); badge.className = "cr-page-badge"; const icon = document.createElement("span"); icon.className = "cr-page-badge-icon"; icon.textContent = "📄"; const label = document.createElement("span"); label.textContent = pageInfo.total && pageInfo.total > pageInfo.page ? "עמוד " + pageInfo.page + " מתוך " + pageInfo.total : "עמוד " + pageInfo.page; badge.appendChild(icon); badge.appendChild(label); sep.appendChild(badge); const nodes = Array.isArray(sourceNodes) ? sourceNodes : [sourceNodes]; const imported = nodes.map((node) => { const clone = document.importNode(node, true); // הסרת סקריפטים כדי למנוע הרצה כפולה/שגיאות. clone.querySelectorAll("script").forEach((s) => s.remove()); // הגנה נוספת: הסרת שאריות מודעות/ווידג'טים שעלולות להיות מוטמעות // בתוך הודעה בודדת (גם אם ברוב המקרים כבר לא נבחרו כלל). clone.querySelectorAll(ContinuousReader.AD_SELECTORS).forEach((el) => el.remove()); // המרת כתובות יחסיות למלאות מול העמוד שממנו נלקח התוכן. this._absolutizeTree(clone, baseUrl); return clone; }); if (dir === "prev") { // עמוד קודם (טעינה אחורה): מציבים את התגית *אחרי* ההודעות, כלומר //ית ה-section — בדיוק בנקודת התפר מול התוכן הישן שכבר היה // בעמוד. כך כשגוללים אחורה "נוחתים" על סוף העמוד החדש שנטען, // עם תגית מספר העמוד נראית למטה במסך (ולא צריך לגלול עוד כדי // לראות אותה בראש כל ההודעות של אותו עמוד). imported.forEach((node) => wrapper.appendChild(node)); wrapper.appendChild(sep); } else { // עמוד הבא (טעינה קדימה): התגית בראש ה-section, כך שרואים אותה // מיד כשמגיעים אליה ואז ממשיכים לקרוא את העמוד מלמעלה למטה. wrapper.appendChild(sep); imported.forEach((node) => wrapper.appendChild(node)); } return wrapper; } _absolutizeTree(root, baseUrl) { const fix = (el, attr) => { const v = el.getAttribute(attr); if (v && !/^(https?:|data:|mailto:|tel:|#|javascript:)/i.test(v)) { const abs = absolutize(v, baseUrl); if (abs) el.setAttribute(attr, abs); } }; root.querySelectorAll("[href]").forEach((el) => fix(el, "href")); root.querySelectorAll("[src]").forEach((el) => fix(el, "src")); root.querySelectorAll("img[srcset], source[srcset]").forEach((el) => { const val = el.getAttribute("srcset"); if (!val) return; const fixed = val.split(",").map((part) => { const seg = part.trim().split(/\s+/); const abs = absolutize(seg[0], baseUrl); if (abs) seg[0] = abs; return seg.join(" "); }).join(", "); el.setAttribute("srcset", fixed); }); } async _loadNext() { if (this.loadingF || !this.forwardUrl) return; if (this.visited.has(this.forwardUrl)) { this.forwardUrl = null; return; } this.loadingF = true; const url = this.forwardUrl; this.visited.add(url); this._updateIndicator("טוען עמוד הבא…"); console.log("[גלילה רציפה] טוען קדימה:", url); try { // בטיחות: אם ה-URL הבא כבר לא עמוד "נושא" אמיתי בפרוג (למשל הופנה // לרשימת פורום או לדף אחר) — לא ממשיכים, כדי לא לגרור מודעות/תוכן זר. if (this.cfg.mode === "xenforo" && !isProgThreadUrl(url)) { this.forwardUrl = null; this._updateIndicator("סוף התוכן"); return; } const doc = await this._getDoc("forward", url); const nodes = this.cfg.mode === "xenforo" ? getXfPostNodes(doc) : getContentElement(doc, this.cfg); if (nodes) { const pageInfo = this.cfg.mode === "xenforo" ? getXfPageInfo(doc, url) : { page: ++this.pageForward, total: 0 }; if (this.cfg.mode === "xenforo") { this.pageForward = pageInfo.page; this.pageTotal = pageInfo.total; } const section = this._buildSection(nodes, url, pageInfo, "next"); this.lastEl.parentNode.insertBefore(section, this.lastEl.nextSibling); this.lastEl = section; this.lastMessageEl = this._lastMessageIn(section); this.sections.push({ page: pageInfo.page, url, el: section }); this.forwardUrl = getNextUrl(doc, url, this.cfg); this._updateIndicator(this._dirLabel()); console.log("[גלילה רציפה] נוסף עמוד", pageInfo.page, "| הבא:", this.forwardUrl || "אין (סוף)"); // התחלת טעינה מוקדמת של העמוד שאחרי, כדי שהמעבר הבא יהיה מיידי. this._prefetch("forward"); // מעבר עדין (לא קפיצה חדה) לתחילת העמוד החדש, כך שהתגית עם // מספר העמוד תיראה מיד בראש המסך. קודם ממתינים שתנופת גלילה // טבעית (אם יש) תירגע, כדי שהיא לא "תילחם" באנימציית הנחיתה // ותגרום לתגית לחלוף מבלי שרואים אותה. this._waitForScrollIdle(() => { this._scrollToSectionTop(section); }); } else { this.forwardUrl = null; this._updateIndicator("סוף התוכן"); console.warn("[גלילה רציפה] בעמוד שנטען לא נמצא אלמנט תוכן — עוצר קדימה."); } } catch (e) { console.warn("[גלילה רציפה] נכשלה טעינת העמוד הבא:", e); this._updateIndicator("שגיאה בטעינה קדימה"); this.forwardUrl = null; } finally { this.loadingF = false; } } async _loadPrev() { if (this.loadingB || !this.backwardUrl) return; if (this.visited.has(this.backwardUrl)) { this.backwardUrl = null; return; } this.loadingB = true; const url = this.backwardUrl; this.visited.add(url); this._updateIndicator("טוען עמוד קודם…"); const s = scroller(); const beforeHeight = s.scrollHeight; const beforeTop = window.scrollY || s.scrollTop; try { if (this.cfg.mode === "xenforo" && !isProgThreadUrl(url)) { this.backwardUrl = null; this._updateIndicator("תחילת התוכן"); return; } const doc = await this._getDoc("backward", url); const nodes = this.cfg.mode === "xenforo" ? getXfPostNodes(doc) : getContentElement(doc, this.cfg); if (nodes) { const pageInfo = this.cfg.mode === "xenforo" ? getXfPageInfo(doc, url) : { page: --this.pageBackward, total: 0 }; if (this.cfg.mode === "xenforo") { this.pageBackward = pageInfo.page; this.pageTotal = pageInfo.total; } const section = this._buildSection(nodes, url, pageInfo, "prev"); this.firstEl.parentNode.insertBefore(section, this.firstEl); this.firstEl = section; this.firstMessageEl = this._firstMessageIn(section); this.sections.unshift({ page: pageInfo.page, url, el: section }); this.backwardUrl = getPrevUrl(doc, url, this.cfg); // התחלת טעינה מוקדמת של העמוד שלפני כן. this._prefetch("backward"); // פיצוי מיידי על השינוי בגובה כדי שהגלילה לא "תקפוץ" ברגע ההוספה. // behavior: "auto" בכוונה - זו קפיצה מיידית שאמורה להיות בלתי // מורגשת; אם לאתר יש scroll-behavior: smooth ב-CSS, קריאה // "סתמית" הייתה הופכת את הקפיצה המיידית הזו לאנימציה נראית // לעין (בדיוק ההפך ממה שהיא נועדה לעשות). requestAnimationFrame(() => { const afterHeight = scroller().scrollHeight; const delta = afterHeight - beforeHeight; window.scrollTo({ top: beforeTop + delta, left: 0, behavior: "auto" }); this._updateIndicator(this._dirLabel()); // ואז מעבר עדין (לא קפיצה) אל סוף העמוד החדש שנטען — כך // ש"נוחתים" על סופו, עם תגית מספר העמוד נראיתית המסך. // גם כאן ממתינים לרגיעת תנופת גלילה, ליתר ביטחון. this._waitForScrollIdle(() => { this._scrollToSectionBottom(section); }); }); } else { this.backwardUrl = null; this._updateIndicator("תחילת התוכן"); } } catch (e) { console.warn("[גלילה רציפה] נכשלה טעינת העמוד הקודם:", e); this._updateIndicator("שגיאה בטעינה אחורה"); this.backwardUrl = null; } finally { this.loadingB = false; } } // "עוקב גלילה" (scroll-spy): קובע איזה עמוד טעון נמצא כרגע בתצוגה, // ומעדכן את שורת הכתובת של הדפדפן לכתובת האמיתית של אותו עמוד באתר — // בלי לרענן ובלי להוסיף רשומה חדשה להיסטוריית "חזור" (replaceState). // כך שורת הכתובת תמיד תשקף את מספר העמוד האמיתי שרואים כרגע, בדיוק // כאילו ניווטתם אליו ישירות באתר. _updateAddressBar() { if (!this.sections.length) return; // קו ייחוס קרוב לראש המסך: העמוד שתחילתו כבר עברה את הקו הזה // נחשב "בתצוגה". const refY = window.innerHeight * 0.25; let current = this.sections[0]; for (const s of this.sections) { const rect = s.el.getBoundingClientRect(); if (rect.top <= refY) { current = s; } else { break; // הרשימה מסודרת לפי סדר המסמך, אז אפשר לעצור כאן. } } if (!current.url || current.url === this._lastUrlSynced) return; try { if (new URL(current.url).origin !== location.origin) return; } catch (e) { return; } this._lastUrlSynced = current.url; try { history.replaceState(history.state, "", current.url); } catch (e) { // מתעלמים משגיאות History API (למשל אם הדפדפן חסם זאת). } } // ------- חיווי צף על המסך ------- // מוצג לזמן קצר בלבד בזמן פעולה (טעינה/שינוי) ונעלם לבד, כדי שלא // "יקפוץ" ויציק לקורא כל הזמן כשגוללים עמוק בתוך התוכן. _createIndicator() { const el = document.createElement("div"); el.className = "cr-indicator"; document.documentElement.appendChild(el); this.indicator = el; } _updateIndicator(text, holdMs) { if (!this.indicator) return; this.indicator.textContent = text; this.indicator.classList.add("cr-indicator-show"); clearTimeout(this._hideT); this._hideT = setTimeout(() => { this.indicator.classList.remove("cr-indicator-show"); }, holdMs || 1400); } } // --------------------------------------------------------------------------- // אתחול: התאמת האתר הנוכחי מול ההגדרות // --------------------------------------------------------------------------- function findSiteConfig(sites) { const host = location.hostname.toLowerCase(); for (const site of sites) { if (!site.enabled) continue; const m = (site.match || "").trim().toLowerCase(); if (!m) continue; // תמיכה בכוכביות פשוטות (glob), אחרת בדיקת "מכיל". if (m.includes("*")) { const re = new RegExp("^" + m.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"); if (re.test(host)) return site; } else if (host.includes(m)) { return site; } } return null; } let reader = null; async function boot() { let stored; try { stored = await store.get(["sites", "settings"]); } catch (e) { return; // אחסון הסקריפט לא זמין } const sites = stored.sites || []; const settings = stored.settings || { direction: "both" }; const cfg = findSiteConfig(sites); if (!cfg) return; // האתר הזה לא מוגדר בסקריפט // שכבת ביטחון: פרוג הוא XenForo — נכפה את המצב הנכון גם אם ההגדרה ישנה. if (/prog\.co\.il/i.test(location.hostname) && cfg.mode !== "xenforo") { cfg.mode = "xenforo"; } // מצב XenForo (פרוג ופורומים דומים) פועל אך ורק בעמוד "נושא" אמיתי // (כתובת עם "/threads/"), כלומר על ההודעות עצמן. עמודי רשימת פורום, // דף הבית וכד' מלאים במודעות ותצוגות מקדימות ואינם רלוונטיים לגלילה // רציפה של פוסטים — לכן הסקריפט פשוט לא יתאתחל בהם. if (cfg.mode === "xenforo" && !isProgThreadUrl(location.href)) { return; } if (settings.direction === "off") { // עדיין נאתחל כדי שאפשר יהיה להדליק מהתפריט בלי רענון. } reader = new ContinuousReader(cfg, settings); await reader.init(); } // --------------------------------------------------------------------------- // תפריט הסקריפט — במקום הפופאפ ועמוד ההגדרות של התוסף // --------------------------------------------------------------------------- // שינוי כיוון גלילה (נשמר + מיושם מיד על העמוד הנוכחי, בלי רענון). async function applyDirection(dir) { const { settings } = await store.get(["settings"]); const s = Object.assign({}, DEFAULT_SETTINGS, settings, { direction: dir }); await store.set({ settings: s }); if (reader) { reader.setDirection(dir); } else { console.warn("[גלילה רציפה] הסקריפט לא פעיל באתר הזה — הכיוון ייכנס לתוקף באתר תומך."); } console.log("[גלילה רציפה] כיוון שונה ל:", dir); } // הוספת אתר חדש (במקום "הוספת אתרים והגדרות" בפופאפ). async function addSitePrompt() { const domain = await gmPrompt( "הוספת אתר — הזינו דומיין (בדיקת \"מכיל\"), למשל: prog.co.il", "" ); if (!domain) return; const d = (domain || "").trim().toLowerCase(); if (!d) return; const { sites } = await store.get(["sites"]); const arr = Array.isArray(sites) ? sites : []; if (arr.some((s) => (s.match || "").toLowerCase().includes(d))) { console.warn("[גלילה רציפה] האתר כבר קיים בהגדרות."); return; } arr.push({ id: "site_" + Date.now(), name: d, match: d, mode: "auto", contentSelector: "", nextSelector: "", prevSelector: "", enabled: true }); await store.set({ sites: arr }); // רענון כדי שהאתר החדש ייכנס לתוקף בעמוד הנוכחי (אם זה הוא). location.reload(); } // הסרת האתר שתאם לדומיין הנוכחי (אם יש כזה). async function removeCurrentSite() { const { sites } = await store.get(["sites"]); const arr = Array.isArray(sites) ? sites : []; const host = location.hostname.toLowerCase(); const remaining = arr.filter((s) => { if (!s.enabled) return true; const m = (s.match || "").trim().toLowerCase(); if (!m) return true; if (m.includes("*")) { const re = new RegExp("^" + m.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"); return !re.test(host); } return !host.includes(m); }); if (remaining.length === arr.length) { console.warn("[גלילה רציפה] האתר הנוכחי לא מוגדר בסקריפט — אין מה להסיר."); return; } await store.set({ sites: remaining }); location.reload(); } // איפוס מלא להגדרות ברירת המחדל. async function resetSettings() { await store.set({ settings: DEFAULT_SETTINGS, sites: [progSite()] }); location.reload(); } // רישום פקודות התפריט של Tampermonkey. function registerMenu() { const dirs = [ ["both", "⇅ קדימה + אחורה"], ["forward", "↓ רק קדימה"], ["backward", "↑ רק אחורה"], ["off", "✕ כבוי"] ]; for (const [d, label] of dirs) { GM_registerMenuCommand("מצב גלילה: " + label, () => applyDirection(d)); } GM_registerMenuCommand("➕ הוספת אתר", () => addSitePrompt()); GM_registerMenuCommand("🗑 הסרת האתר הנוכחי", () => removeCurrentSite()); GM_registerMenuCommand("🔄 איפוס הגדרות", () => resetSettings()); } // --------------------------------------------------------------------------- // התחלה // --------------------------------------------------------------------------- (async function main() { await ensureDefaults(); registerMenu(); await boot(); })(); })();
  • בירור | חיפוש בנטפרי ביוטיוב

    55
    5 הצבעות
    55 פוסטים
    2k צפיות
    ע
    עמי-מחשבים כתב: @elhanan האם יהיה אפשרי להוסיף גירסה עם הורדת סרטון מבוקש מה עם זה זה הכי חשוב