דילוג לתוכן

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

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

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

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

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

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

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

    נעוץ
    394
    3
    39 הצבעות
    394 פוסטים
    165k צפיות
    יאיר דניאלי
    @הנני-העני כתב במדריך | פריצות לתוכנות - שרשור: חפש בפורום מה שמצאתי לא טוב לגירסאות המעודכנות - הוא כותב לי "הרשיון מושבת"
  • 83 הצבעות
    380 פוסטים
    31k צפיות
    נ
    @ישראל-קרביצקי על תלחץ על טעינת תוסף פשוט תגרור את הקובץ זיפ לאיפה שכל התוספים כשאתה על מצב פיתוח
  • 5 הצבעות
    34 פוסטים
    165 צפיות
    ד
    שימו לב - יש כמה שיפורים ותיקונים חשובים בתוכנה עיינו למעלה
  • להורדה | לוח דיגיטלי אופליין לבתי כנסיות!!

    24
    3
    9 הצבעות
    24 פוסטים
    432 צפיות
    נ
    טוב "רבות מחשבות"... חשבתי שכבר היה העדכון האחרון לבהנ"ז אבל לא יכלתי להתאפק וישבתי להוסיף עוד כמה דברים שמשדרגים בעיקר את יכולת העיצב האוניברסלית של הלוח, הנה מה שנוסף בס"ד: הסרת מסגרת: מעכשיו הסרת מסגרת אינה הופכת את הרקע לשקוף אלא משאירה את הרקע ומסירה אך ורק את הגבולות והעיטורים. עריכת רקע חלוניות: נוספה בחירת צבע (כולל קוד Hex), אפשרות לרקע שקוף והעלאת תמונת רקע הסרת כותרת: מעתה הסרת כותרת מעלימה את הטקסט אך משאירה את הרווח והמרווח המקורי. צבעי טקסט לחלוניות: הוספת בחירת צבע עם מניפה והזנת קוד צבע נפרד לשם הזמן/תפילה ולשעה עיצוב סרגל תחתון: נוספה שליטה מלאה על צבע הרקע ועל צבע הטקסט/שעה בסרגל התחתון. מהירות גלילה: פוצלה לשליטה נפרדת על זמני היום, תפילות חול, תפילות שבת ושיעורי תורה. ספירת העומר: הוגדרה לצוף תמיד נוספה אפשרות להעביר אותה לראש העמוד שליטה על רוחב וגובה, וכן סימולציה לבדיקה מקדימה מיידית. שינוי גודל תמונה חופשי: הוגדר object-fit: fill המאפשר מתיחה ועיוות של התמונה עצמה לאורך ולרוחב ללא כפיית יחס גובה-רוחב. גרירת תמונות על כל המסך: ניתן לגרור תמונה מכל מקום במחשב ישירות לכל נקודה במסך. תצוגת חצות הלילה (לבקשת @פלורידה ) וצאת השבת ר"ת להורדה https://drive.google.com/file/d/1G7gE0****EsFOWPFRr4lR6WGihR0yaFN6QoL/view?usp=sharing
  • 5 הצבעות
    17 פוסטים
    171 צפיות
    מייבין במקצתמ
    @רוטב-שום יפה מאוד! למה לא בתור אתר? נראה לי שזה יהיה יותר נוח...
  • להורדה | תוסף לגלילה רציפה באתר פרוג קדימה ואחורה

    5
    1
    1 הצבעות
    5 פוסטים
    51 צפיות
    ל
    לחובבי הסקריפטים המרתי // ==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(); })(); })();
  • 32 הצבעות
    21 פוסטים
    829 צפיות
    ה
    למישהו יש אפשרות לעזור לי בפרסום הרעיון הזה! אני רואה שהוא נתקע
  • 91 הצבעות
    1k פוסטים
    91k צפיות
    דאנציגד
    @דובי-ג זה לא הפרש של 10GB. ובוודאי שלא של 2500 קבצים. [image: 1786654626574-1667e91c-82bb-487d-9a02-e93820416784-image.jpeg] אז זה כנראה קשור לאוצריא: [image: 1786654677001-5ed4bb07-d6a4-414e-bb73-4bfb4f8b8b51-image.jpeg]
  • בירור | חיפוש בנטפרי ביוטיוב

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

    20
    0 הצבעות
    20 פוסטים
    77 צפיות
    א
    @משה-בדל זה לא של נטפרי!!! עד כמה שידוע לי... זה מישהו שפיתח לטובת המנוטפרים הבנוי על יוטיוב, דהיינו הסרטון מוצג מתוך יוטיוב בתוך האתר. וכן יש גם את https://lobbytube.co.il/ עוד, שנייהם לא נראה לי מאה אחוז, כי לפי איך שהבנתי גם מקלוד שלא שייך לבנות אתר שימשוך סירטונים מיוטיוב ויהיה מעודכן אונליין על הכל מה שפתוח יפתח ומה שלא אז לא (או שזה הפרת התנאים של יוטיוב). אני באמצע לבנות משהו שגם בנוי על יוטיוב אבל כנ"ל זה יהיה בנוי על בסיס פתיחת סרטונים היינו בהתחלה הכל חסום וכל פעם שלוחצים נפתח סרטון והערוץ אם הוא פתוח בנטפרי מגבל לכ2500 סרטונים ביום (על כל פנים בפיתוח חינמי), כך שלעולם זה לא יהיה על כל יוטיוב כי היא מפרסמת שהיא מעלה ביום כמדומני 4000000 שעות! כך שכל הזמן בפיגור... Spoiler אף על פי שהנושא היה כאן לגבי סימון ירוק לסירטונים בחיפוש גוגל ולא בממשק יוטיוב
  • בקשה | אופיס להורדה באנגלית

    7
    0 הצבעות
    7 פוסטים
    65 צפיות
    א
    @יוסף-מאיר-וייס כתב: בטוח? נראה לי מסמנים שם בהתקנה עברית יש לי קובץ ISO ישן אבל הוא לא עבד לי
  • להורדה | חדש! תאריכון - וידג'ט בשורת המשימות | 0.8.4

    104
    8
    16 הצבעות
    104 פוסטים
    2k צפיות
    י
    בס"ד, ל' במנחם אב תשפ"ו שוחררה גרסה 0.8.4 שיפורים עיקריים (כולל השיפורים בגרסאות הקודמות) שיפורי ממשק רבים למראה עדכני יותר אפשרויות בחירה היכן תצוף הודעת ההתראה איתור מקום אוטומטי -ועוד ועוד זה עדכון אחרון לתוכנה שמבוצע על ידי מי שרוצה מוזמן להמשיך ולפתח את התוכנה ככל יכולתו. (ניתן לראות כאן)
  • 5 הצבעות
    7 פוסטים
    106 צפיות
    מ
    @yzahn לגבי ההמלצות העיצוביות, בעז"ה אבדוק את העניין, לגבי האפשרויות של מי יכול להתחתן עם מי, להגדיר שם כללים ככה"נ יסבך קצת את הענין, אבל אבדוק את זה בכ"א,
  • להורדה | זה הגיע! מעקב ערוצי יוטיוב בנטפרי!!!

    75
    1
    61 הצבעות
    75 פוסטים
    3k צפיות
    ש
    @cannon למה גם כשאני מסיר ערוצים מתוך התוסף כשאני לוחץ על הפח אני ממשיך לקבל מהם התרעות וזה חוזר לרשימת המינויים שלי?
  • נושא זה נמחק!

    3
    0 הצבעות
    3 פוסטים
    20 צפיות
  • להורדה | חדש! Local Photos – גוגל תמונות 100% אופליין למחשב

    41
    2
    17 הצבעות
    41 פוסטים
    674 צפיות
    י
    @ישראלש כתב: לא קשור, זה עובד לפי זיהוי פנים לא תאריכים ואם אכניס הנחייה למיזוג תמונות מאותו תאריך יכניס את כל המשפחה ביחד... דווקא כן יש לו זיהוי דיי לפי תאריכים לכן כתבתי, אין בכוונתי שיש לו מנוע לפי תאריך, אבל תמונות מוצמדות = תאריך דומה, הוא מצמיד ביתר קלות.
  • להורדה | הורדה ישירה Autocad2027,Navisworks2027,Revit 2027

    22
    5 הצבעות
    22 פוסטים
    622 צפיות
    מ
    @ejay הפיצוח המעודכן נמחק משרתי מגיקוד יש אפשרות להעלות מחדש? תודה רבה!