ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
הדף השבועי של בני עקיבא — תשפ"ז אפליקציה סטטית (PWA) לתוכנית הדף השבועי: דף בשבוע, מסכת שלמה בשנה. כל ישיבה בוחרת מסכת תענית, מסכת מגילה, או את שתיהן. הכול ב-data.js. אין צורך לגעת בקוד. מייצאים את קובץ ה-PowerPoint ל-PDF, וממירים לתמונות: התוצאה 1920×1080, בערך 180KB לשקף. אחר כך מוסיפים deck: ל-CONTENT.檢視完整 README ↗
אפליקציה סטטית (PWA) לתוכנית הדף השבועי: דף בשבוע, מסכת שלמה בשנה. כל ישיבה בוחרת מסכת תענית, מסכת מגילה, או את שתיהן.
| קובץ | תפקיד |
|---|---|
index.html | האפליקציה כולה — מסכים, מנוע המצגת, החידה |
data.js | כל התוכן. לוח השנה, המוסדות, המצגות והחידות |
links.js | קישורי צורת הדף והחברותא, מיובא מ"יגדיל תורה" |
deck.js | המצגות — הקבצים בריפו, הסדר בגיליון. מקור אחד לכל המסכים |
slides/ | שקפי המצגות כתמונות, תיקייה לכל שבוע |
sfarim/ | תמונות הגמרות — כריכה ופריסת עמודים לכל מהדורה |
logo.js | הלוגו הרשמי של בני עקיבא, מוטמע כ-base64 |
vercel.json | ניתוב /admin וכלל מטמון |
sw.js | Service Worker — פתיחה מיידית ועבודה בלי רשת |
manifest.json | הגדרות PWA להתקנה במסך הבית |
admin-manifest.json | אותו דבר למסך הניהול — אפליקציה שנייה, אייקון זהב |
studio.html | סטודיו הדף — זיהוי אזורים ושורות, לדף האינטראקטיבי |
learn.html | מסך התלמיד — הדף קטע אחר קטע, לפי מה שסומן בסטודיו |
vendor/pdfjs/ | pdf.js, כדי שהסטודיו והלימוד יפתחו קובצי דף ישירות |
daf/ | צורות הדף כתמונות — מה שהאתר מגיש לתלמידים |
tools/build-library.py | בונה את daf/ מהדרייב, דרך הסקריפט של הרכז |
tools/build-daf.py | ממיר דף בודד מקבצים מקומיים |
apps-script.gs | הגשר לגיליון. גנרי — נפרס פעם אחת ולא נוגעים בו שוב |
הכול ב-data.js. אין צורך לגעת בקוד.
CONTENT['megila-7'] = { // taanit או megila, ומספר השבוע (1–36)
title: 'כותרת הדף',
deck: { dir:'slides/megila-7', n:12 } // מצגת תמונות — העדיפות
};
QUIZ['megila-7'] = { q:'השאלה', a:['א','ב','ג','ד'], correct:1 };
מייצאים את קובץ ה-PowerPoint ל-PDF, וממירים לתמונות:
mkdir -p slides/megila-7
pdftoppm -jpeg -jpegopt quality=80 -r 96 deck.pdf slides/megila-7/s
cd slides/megila-7 && i=1
for f in $(ls s-*.jpg | sort -V); do mv "$f" "$(printf '%02d.jpg' $i)"; i=$((i+1)); done
התוצאה 1920×1080, בערך 180KB לשקף. אחר כך מוסיפים deck: ל-CONTENT.
כוונו ל-1920×1080. נגן המצגת ממלא את המסגרת ושומר על היחס
(object-fit:contain), ולכן שקף קטן מהמסך נמתח מעט. slides/megila-1
הוא 1920×1080 — זו המידה הנכונה. slides/taanit-1 נשאר 1376×768
ונמתח ×1.19 על מקרן, וזה עדיין בסדר גמור; מתחת לזה כבר יורגש.
היה כתוב כאן שאין צורך להגדיל, "כי הנגן הוא object-fit:contain".
זה היה נכון בכוונה ולא בפועל: לתמונה לא היה רוחב וגובה, ולכן
object-fit לא עשה דבר ושקף קטן מהמכל נשאר בגודלו — שקפי תענית
מילאו 51% ממקרן 1080p. תוקן ב-#d-img.
אם אין deck אבל יש slides: [{k,h,t},…] — האפליקציה מרנדרת מצגת טקסט
בעיצוב התוכנית. שימושי לשבוע שאין לו עדיין מצגת מעוצבת. כששניהם
קיימים deck מנצח, וה-slides נשאר כגיבוי.
הכפתורים במסך הבית נגזרים מזה. שבוע עם deck מקבל
"▶ הצגה על המסך" + "הדף"; בלעדיו — "לדף ולחומרי הלימוד" + "המסלול".
אין קוד נפרד לכל מסלול.
מעלים את APP_VERSION ב-index.html וגם את CACHE_NAME
ב-sw.js, לאותו מספר. שניהם, ידנית.
כאן היה כתוב ש-CACHE_NAME נגזר אוטומטית בסקריפט הבנייה. אין
סקריפט כזה — אין בפרויקט הזה שלב בנייה בכלל, ו-tools/ מחזיק
רק את כלי המרת הדפים. השורה הזו הבטיחה שמירה שלא התרחשה.
וזה לא פרט טכני: גרסת מטמון שנשכחה גורמת למכשיר להגיש
index.html ישן יחד עם data.js חדש, וזה שובר מסכים שלמים.
בנוסף, CACHE_NAME הוא מה שמוחק את המטמון הישן — בלי העלאה
שלו, מכשיר על חיבור איטי ימשיך להגיש את הקוד הישן מהמטמון גם
אחרי שהעלינו גרסה חדשה (ראו פסק הזמן של 2.5 שניות למטה).
tools/preflight.py בודק שהשניים תואמים — וגם ששקפי המצגות
וקובצי הדף שמוזכרים בקוד באמת קיימים. הוא רץ אוטומטית בכל
דחיפה (.github/workflows/check.yml), ואפשר להריץ אותו גם ביד:
python3 tools/preflight.py
אסטרטגיית המטמון: קבצי קוד (index.html, *.js) נטענים
מהרשת קודם ורק בכישלון מהמטמון — כך גרסה תקועה לא יכולה
להישאר. ל"מהרשת קודם" יש פסק זמן של 2.5 שניות (NET_TIMEOUT),
וזה לא קישוט: fetch על חיבור תקוע אינו נכשל אלא תלוי עשרות
שניות, ואז data.js לא מגיע, boot() לא רץ, ומסך הכניסה נתקע
על הלוגו. עכשיו העותק השמור מנצח את השעון, והרשת מעדכנת ברקע. שקפים ואייקונים נטענים מהמטמון קודם, כי הם כבדים ולא
משתנים בשקט. אופליין ממשיך לעבוד בשני המקרים.
/adminאין אליו שום קישור באפליקציה. נגיש רק בכתובת ישירה:
…/admin (או …/#admin), ואחריה קוד בן 4 ספרות.
/admin עובד דרך admin.html — קובץ אמיתי שמפנה ל-/#admin,
ולא דרך כלל rewrite. הסיבה: cleanUrls ב-vercel.json הופך יעד
rewrite של /index.html ללא-תקף, ו-/admin החזיר 404. קובץ בשם
admin.html נפתר על ידי cleanUrls עצמו, בלי להסתמך על סדר
הכללים. אל תמחק אותו לטובת ה-rewrite.
ברירת המחדל ב-ADMIN_PIN שב-data.js, וניתן לשנות אותה מתוך המסך.
שבעה מקטעים: הרשמות שהתקבלו · מוסדות (מי מופיע כ"בפנים") · מוקד שיחות · טקסטים · מחירים · הסוגיה היומית · הגדרות.
כל הכתיבות עוברות דרך Store. להחלפת המנוע לשרת מרוחק צריך
למלא Store.push — שאר הקוד לא משתנה.
אנשי הקשר אינם בקוד. הריפו ציבורי, ולכן מספרי הטלפון יושבים בגיליון גוגל, והמכשיר מחזיק רק עותק מהיר (כדי שאפשר יהיה לתעד גם באמצע שיחה בלי קליטה). המוקד קורא וכותב שלוש לשוניות:
איזה גיליון: המזהה לא כתוב בריפו. הוא נשמר במכשיר של הרכז
(ניהול ← הגדרות ← "מזהה גיליון אנשי הקשר"), ובשרת ב-Script
Properties בשם CONTACTS_ID (ריק = הגיליון של PRIVATE_ID).
אם הוא ריק גם במכשיר, הקריאה והכתיבה הולכות לגיליון הראשי —
וזה גיליון שפתוח לקריאה לכל מי שיש לו הקישור.
לכל מוסד: חיוג ישיר, וואטסאפ, סטטוס (לא דיברתי / דיברתי / מתעניין / הצטרף / לא השנה) והערה חופשית. המוסדות שלמדו בתשפ"ו מוצגים ראשונים.
/admin ← טקסטיםכל המלל של מסך ההרשמה ניתן לעריכה בלי לגעת בקוד. שלוש שכבות, כל אחת דורסת את שלפניה:
| שכבה | מקור | מי רואה |
|---|---|---|
| ברירת מחדל | WAYS, UI, SFARIM ב-data.js | כולם |
| פורסם | לשונית טקסטים בגיליון | כולם |
| טיוטה | CFG.texts ב-localStorage | המכשיר הזה בלבד |
TEXT_FIELDS ב-data.js הוא הרשימה שמייצרת את המסך: k נתיב
לתוך האובייקטים, lbl מה שהרכז רואה, ml שדה רב-שורתי, g
כותרת קבוצה. הוספת שורה שם מוסיפה שדה במסך, בלי קוד.
שמירה ≠ פרסום. עריכה משנה את המכשיר של הרכז מיד, כדי שיראה איך זה נראה. רק פרסום כותב לגיליון, ומשם זה מגיע לכולם בטעינה הבאה. ההפרדה מכוונת: אפשר לנסח, להסתכל, ורק אז לשחרר.
שדה ריק חוזר לנוסח שבקוד — TEXT_BASE שומר את הערכים המקוריים
בטעינה הראשונה, ולכן "שחזור" תמיד חוזר לקוד ולא לערך שנדרס.
מחרוזות עם משתנים עוברות דרך fill(): {n} הכמות, {name} שם
המהדורה, {sum} הסכום. משתנה שנמחק מהנוסח פשוט לא יוצג.
מגבלה שכדאי לזכור: הפרסום עובר ב-mode:'no-cors' ולכן אין
אישור מהשרת. אם ה-Apps Script לא עודכן לגרסה שתומכת ב-texts,
השליחה תיראה מוצלחת ולא יקרה כלום. בדיקה: הגדרות ← בדיקת
חיבור, ואחריה הלשונית "טקסטים" בגיליון.
FIT יושב בעמוד הראשי ולא במסך ההרשמה, ולכן admTextSet()
מצייר מחדש גם את הבית — אחרת עריכה של אותן שורות הייתה נראית
רק אחרי ריענון.
אין שיוך לכיתות. כל ישיבה בוחרת מסכת אחת — תענית או מגילה —
או את שתיהן. TRACKS ב-data.js מחזיק רק masechet, ואין באפליקציה
שום אזכור של כיתה ז׳ או ח׳.
"מי אתם" הוא אחרון בכוונה: אף אחד לא משאיר שם וטלפון לפני שהוא
יודע למה, והחששות נענים בדרך למטה. למי שכבר נרשם מופיעה למעלה
שורת .jump שגוללת ישר לסעיף 3.
המסך נפתח ישר בבחירה, בלי הסברים. מי שהגיע לכאן כבר החליט, וההסבר "למי מתאימה התוכנית" ישב כאן פעם ורק האריך את הדרך.
FIT ב-data.js, ומוצג בעמוד הראשי — אחרון, ומיד אחריו
כפתור ההרשמה. שלוש שורות בלי תיאורים: הקורא מזהה את עצמו באחת
מהן וממשיך לכפתור.
FIT.items[].icon בוחר סמל מתוך FIT_ICONS ב-index.html.
הסמלים מצוירים ב-SVG בקוד ולא כקבצים — ובכוונה אין כאן איור.
באפליקציה כולה אין ולו איור אחד (רק הסמל של בני עקיבא וצילומי
הגמרות), ואיור בודד היה נראה מודבק. קו זהב דק שומר על השפה.
SFARIM[].rec הוא המשפט שמופיע לראש החטיבה, בגוף שני, כפי שהוא
נאמר בטלפון. מתחתיו הספר: הכריכה בגודלה הטבעי (pics[].small —
מתיחה מעבר לו רק מרככת אותה), ומתחתיה הפריסה הפתוחה ברוחב מלא.
זו התמונה שממנה מתרשמים. fit הוא אותו דבר בשתי מילים, לשורת
ההזמנה.
לחיצה על תמונה מגדילה אותה לשכבת #zoom, ושם היא נפתחת
בגודלה המקורי ולא מוקטנת למסך: בנייד "התאמה למסך" הייתה
מחזירה אותה כמעט לגודל שבכרטיס, ואז ההגדלה לא מוסיפה כלום. גדולה
מהמסך → גוללים אליה.
שני פרטים שלא כדאי לשבור:
margin:auto על התמונה, לא justify-content:center
על המכל. מרכוז ב-flex חותך את הקצה ההתחלתי כשהתוכן גדול מהמכל,
והחלק הזה נעשה בלתי נגיש בגלילה.*-page.jpg ברוחב 900 לכרטיס,
ו-*-page-hi.jpg ברזולוציית המקור להגדלה (pics[].hi). בלי זה
ההגדלה מטושטשת — אין מה להגדיל מעבר ל-900 פיקסל.היה פעם מסך ספרים נפרד; הוא נמחק כי שום כפתור לא הוביל אליו, ולכן התמונות פשוט לא נראו לאיש.
התמונות ב-sfarim/ הן JPEG. קובץ מקור ב-AVIF או WebP יש להמיר
— ספארי בגרסאות ישנות לא מציג אותם, וראש חטיבה אחד יראה ריבוע ריק.
PRICES ב-data.js נותן לכל מהדורה שלושה מספרים:
veshinantam: { price: 15, list: 25, min: 400 },
sugya: { price: 22, list: 25, min: 300 }
price — בהזמנה מרוכזת · list — המחיר הרגיל ·
min — כמה עותקים צריך כדי שההנחה בכלל תינתן.
כל מסך שמציג מחיר עובר דרך priceOf(id), ואין באפליקציה שום סכום
קשוח. min נספר על שתי המסכתות יחד, כי ההוצאה סופרת עותקים
ולא כרכים — זה מה ש-sefTotal() עושה.
condNote() מדפיסה את התנאי צמוד למחיר של המהדורה שלו, בתוך
כרטיס ההמלצה. בלי זה מבטיחים לראשי החטיבות מחיר שעדיין לא הובטח.
שתי ההערות אינן נערמות זו אחרי זו בתחתית — כל אחת ליד הספר שלה.
min:0 מבטל את ההערה לאותה מהדורה.
מלבן הסיכום מציג את הסכום ואת החיסכון בלבד. פירוט הכמויות נמצא בשורות שמעליו, וחזרה עליו רק מעמיסה.
במסך הניהול, לשונית מחירים: שלושה שדות לכל מהדורה. הערכים
נשמרים ב-CFG.prices ו-applyCfg ממזג אותם לתוך PRICES
שדה-שדה, כדי שהגדרה ישנה שנשמרה במכשיר לא תמחק שדה שהתווסף מאז.
reg.qty ממופתח '<מסכת>-<מהדורה>' — ארבעה מספרים. זו גם התשובה
ל"כמה תלמידים", ולכן אין שאלה נפרדת. הפילוח לפי מסכת הוא מה שקובע
כמה כרכים מזמינים מכל אחת.
אין שדה הערה חופשית: מה שצריך לברר מתברר בשיחת הטלפון.
regMigrate() מטפל בשתי צורות ישנות של הרשמה שמורה: z/ch עם
מהדורה אחת (z היה תענית ו-ch מגילה, ולכן ההעברה מדויקת), ו-qty
לפי מהדורה בלבד בלי מסכת — צורה שחיה שעות ספורות, ואין בה מידע על
המסכת, ולכן היא נכנסת לתענית וצריך לוודא אותה בשיחה.
?m=<code> משייך את מי שנכנס למוסד שלו מראש — למשל ?m=raanana.
הקודים נמצאים ב-INSTITUTIONS בתוך data.js.
APPS_SCRIPT_URL ב-data.js מצביע על ה-Web App שקוד המקור שלו
ב-apps-script.gs. הרשמות נכתבות ללשונית "הרשמות", תשובות חידה
ללשונית "חידות" — שתיהן נוצרות לבד בפעם הראשונה.
הסקריפט טיפש בכוונה, וזו הנקודה. הוא אינו יודע אילו עמודות
יש בהרשמה: האפליקציה שולחת cols — מערך מסודר של [כותרת, ערך] —
והסקריפט מוצא כל כותרת בשורה 1 או מוסיף אותה בסוף. לכן מסכת
או מהדורה חדשה מייצרות עמודה חדשה בגיליון בלי לפרוס את הסקריפט
מחדש. regCols() ב-index.html הוא מקור האמת היחיד לשמות.
השדות השטוחים (taanit-veshinantam וכו') נשלחים לצידו בכוונה,
כדי שגם פריסה ישנה של הסקריפט תקלוט הרשמה. באותה מידה הסקריפט
החדש עדיין מקבל את הצורה הישנה — אף הרשמה אינה הולכת לאיבוד
בחלון שבין השניים.
השליחה היא mode:'no-cors' כי Apps Script אינו מחזיר כותרות CORS.
לכן אין אישור מהשרת: כשל רשת משאיר את הפריט בתור וניסיון חוזר
יתבצע אחר כך, אבל שגיאה בצד השרת תיראה כהצלחה. לכן יש "בדיקת
חיבור" בניהול ← הגדרות: doGet מחזיר SCRIPT_VERSION ואת
הלשוניות שהוא רואה, ו-SCRIPT_MIN באפליקציה קובע מה נדרש. גרסה
נמוכה מ-SCRIPT_MIN = הפריסה בגוגל ישנה.
הכתובת חייבת לשבת ב-data.js. שדה "כתובת שרת" במסך הניהול
נשמר על מכשיר אחד בלבד, וראשי החטיבות פותחים במכשירים שלהם.
עמוד Artifact נעטף אוטומטית ב-<!doctype html><head></head><body>.
לכן אין לשלוח מסמך HTML שלם — התגיות הפנימיות מבוטלות, .app
מאבד את הקשר ה-flex שלו, והפריסה נשברת. יש להפיק קטע בלבד:
ה-<style> ותוכן ה-<body>.
מאותה סיבה dir="rtl" שעל תגית <html> נעלם, ולכן העמוד קובע
אותו מהקוד בתחילת ה-body.
manifest.json (display:standalone) + sw.js + האייקונים.
ספארי מתעלם מהאייקונים שב-manifest, ולכן ב-<head> יושבים גם
apple-touch-icon, apple-mobile-web-app-capable
ו-apple-mobile-web-app-title. בלעדיהם "הוספה למסך הבית" באייפון
יוצרת אייקון של צילום מסך.
קבצים תקינים אינם מספיקים — צריך גם להציע. בלי הצעה
האפליקציה נפתחת כחלון עצמאי בלי אייקון, וכשסוגרים אותה צריך
לחפש את הקישור מחדש. initInstall() תופס את beforeinstallprompt
(אנדרואיד/כרום), מונע את הבאנר של הדפדפן ומציע במקומו כפתור
אמיתי אחרי INST_DELAY.
ספארי באייפון אינה מאפשרת התקנה מתוך הדף בשום צורה, ולכן שם
מוצגת רק ההוראה "שיתוף ← הוספה למסך הבית" — ובלי כפתור שמתחזה
להתקנה. מי שהתקין (instDone) או שהאפליקציה כבר רצה כ-standalone
לא רואה כלום; מי שדחה מקבל שקט לשבוע (INST_SNOOZE).
admin-manifest.json הופך את מסך הניהול לאפליקציה שנייה, עם
אייקון זהב ושם "ניהול". id שונה הוא מה שמאפשר את זה: בלעדיו
המערכת הייתה רואה אפליקציה אחת שמחליפה את השנייה.
הבחירה נעשית בסקריפט שבראש ה-<head>, ולא בהמשך הקוד.
הדפדפן קורא את ה-manifest פעם אחת בטעינה, והחלפת ה-<link>
מאוחר יותר אינה נקלטת. אותו סקריפט קובע גם את
apple-touch-icon ואת apple-mobile-web-app-title.
המלכודת: ברגע שהאפליקציה הרגילה מותקנת, כרום מוריד את
"הוספה למסך הבית" מהתפריט שלו ומפסיק לירות beforeinstallprompt
— גם לאפליקציה השנייה. לכן אי אפשר להסתמך על ההתקנה כדי
לקבל את אייקון הניהול.
ואין ל-manifest.json שדה shortcuts, בכוונה. קיצור ל-#admin
היה הדרך היחידה לקבל את האייקון במצב הזה — אבל הוא מציג "ניהול"
לכל מי שלוחץ לחיצה ארוכה על האייקון, כלומר מכריז על קיומו של מסך
הניהול בפני 24 ראשי חטיבות. אי-הנראות היא חלק מההגנה, ולכן
ויתרנו על הנוחות. אל תחזיר אותו.
מי שרוצה את אייקון הזהב צריך להתקין את הניהול לפני האפליקציה הרגילה, בזמן שכרום עדיין מציע התקנה. אחרת — הקישור השמור עושה את אותה עבודה.
בניהול ← הגדרות מוסבר כל זה, ויש "העתקת הקישור לניהול" שתמיד
עובדת. הבאנר הצף אינו מופיע שם (wantsBanner()) — הוא
נועד לראשי החטיבות. כפתור התקנה מוצג רק אם הדפדפן באמת נתן
אירוע; כפתור שלא יעשה כלום גרוע מהוראה.
admRefresh() באותו מסך מוחק את המטמון ומבטל את רישום ה-Service
Worker ואז טוען מחדש — הדרך לוודא שמכשיר לא נתקע על גרסה ישנה.
localStorage נשאר, ולכן הגדרות והרשמות אינן נמחקות.
לאפליקציית הניהול המותקנת אין שורת כתובת, ולכן "לאפליקציה →"
הייתה דרך ללא חזרה. #adm-chip הוא השבב שמחזיר, והוא מופיע רק
כשהאפליקציה נפתחה כניהול וגם רצה כ-standalone.
/studioתשתית לדף האינטראקטיבי. בוחרים מסכת, דף ועמוד מהמאגר, והסטודיו מזהה את מבנה הדף בכ-200ms ומציג אזורים ושורות לאישור או לגרירה.
הבורר נשען על לוח השנה של האפליקציה. CAL_TAANIT ו-CAL_MEGILA
ממפים שבוע לפרשה ולדף, ו-DAF_LINKS נותן לכל דף את שני הקבצים
שבדרייב. אין כאן מאגר חדש — בוחרים שבוע, והקובץ נפתח.
המפתחות ב-DAF_LINKS הם בלי גרשיים, ולכן החיפוש עובר דרך
dafKey() — אותה נורמליזציה שבאפליקציה.
הקובץ מגיע דרך ה-Apps Script, ולא ישירות מהדרייב. הסטודיו
קורא פיקסלים כדי לזהות טורים ושורות, ודפדפן חוסם קריאת פיקסלים
מתמונה חוצת-דומיין; drive.google.com גם אינו שולח כותרת CORS,
ולכן fetch אליו נחסם. הסקריפט רץ בחשבון של הרכז, קורא מהדרייב
שלו ומחזיר את הקובץ (doGet?file=<id>, גרסה 4 ומעלה). הקבצים
נשארים פרטיים ואינם מתפרסמים בשום מקום. אם ה-CORS חוסם גם את
הסקריפט — יש נפילה ל-JSONP, כמו בבדיקת החיבור.
vendor/pdfjs מצורף ואינו נטען מרשת חיצונית, כדי שהסטודיו
יעבוד גם בלי חיבור. "פתיחת קובץ" נשאר לקובץ שאינו במאגר.
ערך האפשרות בבורר הוא אינדקס, לא שם הדף. שמות הדפים מכילים
גרשיים — י"ט — וכשהם נכתבו כערך, המרכאה סגרה את הערך באמצע
והדפדפן קרא י בלבד. כל דף עם גרשיים נטען כדף חד-אותי, וזה
נראה כתקלת טעינה אקראית. selVal() מחזיר את המחרוזת האמיתית.
לכל בקשה מספר, ורק האחרונה מתקבלת. משיכה מהדרייב לוקחת שניות, ולכן החלפת דף בזמן שהקודם עדיין בדרך הציגה את מי שהגיע אחרון ולא את מי שנבחר אחרון — ולפעמים כלום. הפאנל מציג "מוצג כעת", כדי שאי-התאמה כזו תיראה מיד ולא תתגלה בעבודה.
מבנה הדף בדרייב. לכל דף שני קבצים: צורת הדף בשני עמודים (עמוד א׳ וב׳) וחברותא בעשרה עמודי A4.
הקבועים נגזרים ממרווח השורה ולא מרוחב התמונה. זה נתפס כשהתווסף
נתיב ה-PDF: pdftoppm ו-pdf.js מציירים את אותו עמוד בקנה מידה
שונה, וקבועים שנקשרו לרוחב 2539 נעשו שם צרים מדי — המרזבים
החלו להבהב ודף יצא בשבעה אזורים במקום בשניים. מרווח השורה הוא
היחידה הטבעית של הדף. הזיהוי רץ בשני מעברים: הראשון מודד את
המרווח, השני משתמש בו. אימות: בשני הנתיבים השדרה יוצאת
ב-0.298 עד 0.701 מרוחב הגוש המרכזי — אותם יחסים בדיוק.
מרווח שורה (המרחק בין תחילות שורות, לא גובה הרצף) מפריד את הדף לשלוש שכבות, ברוחב ייחוס 2539px:
| מרווח | |
|---|---|
| שוליים — מסורת הש"ס, תורה אור, הגהות | 21–22px |
| רש"י ותוספות | 29–30px |
| גמרא | 33px |
הפער שוליים↔גוש מרכזי הוא 35% ואפשר לסמוך עליו לזיהוי הגוש. הפער גמרא↔פירוש הוא 11% בלבד — צר מכדי לזהות בו את השדרה, והוא משמש להכרעה אחת: האם הגוש שבתחתית הדף הוא עדיין גמרא, או שהגמרא נגמרה והפירושים התפרשו על כל הרוחב.
הגיאומטריה זהה בכל הדפים: הגוש המרכזי 360–1524, השדרה
706–1177. הסדרה מוקלדת אחיד, ולכן הגבולות נצמדים לרשת, ובדף
שבו תוספות קצר והמרזב אינו שורד — היחס הידוע (0.298 ו-0.702
מרוחב הגוש) מחזיר את הגבול הנומינלי.
הגמרא אינה מלבן: למעלה רש"י ותוספות חוצים את המרכז ואין גמרא כלל, באמצע טור צר, ואז מדרגה — ימינה, שמאלה או שתיהן — כשטור פירוש נגמר. כל הכישלונות בבנייה היו בגבול, אף אחד לא היה בשורות. לכן הזיהוי מחזיר אזורים: קטעי גובה שבכל אחד הגבולות קבועים. דף יוצא 2–4 אזורים, והם מה שמאשרים או גוררים.
שני כללים שאסור לשבור, ושניהם מסומנים בקוד:
חברותא אינו דף וילנא אלא A4 בטור אחד, ולכן יש לו נתיב נפרד:
analyseSimple מחזיר פסקה כאזור.
בדפוס וילנא נהוג לשים בתחתית טור את המילה שפותחת את העמוד הבא, מרחפת בתוך הרווח — "איזהו", "יתיב", "אליה". היא שברה את הזיהוי בשתי דרכים: מילאה את הרווח שאמור להפריד בין הגמרא לגוש שמתחתיה, כך שהגמרא נראתה כממשיכה; והיא יושבת בטור אחר מהשורה שלידה, כך ששורה צרה נמדדה כרחבה.
isFloating() מזהה אותה בלי לקרוא: שורה אמיתית פרושה על רוב
רוחב הטור, ומילה בודדת מרוכזת בכתם צר. שורה כזו אינה נספרת
כשורה, ואינה נחשבת עדות לקיום מרזב או להיעדרו — ההחלקה מדלגת
עליה במקום לספור אותה כהיעדר.
הגמרא נמצאת בכל אזור, בכל צד בנפרד, באחד משני מצבים: על המסגרת הפנימית (שני הרווחים הקבועים משני צדיה, שמתחילים אחרי ארבע שורות של רש"י ותוספות) או מורחבת אל המסגרת החיצונית, כשטור פירוש נגמר.
לכן אין בממשק גרירה חופשית של הרוחב אלא מתג לכל צד. זה מדויק יותר, זה בלחיצה אחת, וזה עובד באצבע.
המסגרת הפנימית נאכפת מול היחס הידוע. נמדד בארבעה דפים ובשני מנועי ציור: השדרה יושבת ב-0.298 עד 0.702 מרוחב הגוש, בסטייה של אלפית. קצה שסוטה ביותר מ-6% מוחזר ליחס. בתחילה הגנתי רק על המקרה שהשדרה יצאה רחבה מדי; ההפוך לא נתפס, ובדף ב׳ ע"ב מרזב מדומה בתוך הגמרא הצר אותה בחצי וחתך את כל האזורים באמצע הדף.
הגובה נערך שורה־שורה, ולא בפיקסלים. הגבול העליון נצמד לתחילות שורות והתחתון לסופיהן — כשהתחתון נצמד לתחילות, כל צעד בלע גם את הרווח שאחרי השורה, ואז שורה והמילה המרחפת שאחריה נכנסו ויצאו תמיד ביחד. והצעד נספר לפי שורות שנכללות בפועל ולא לפי המרחק מהגבול: הגבול יושב בדרך כלל מתחת לשורה האחרונה, ולכן צעד "לפי הסוף הקרוב" רק הידק אליה ולחיצה ראשונה לא עשתה כלום.
באג הגרירה שהיה כאן: כל תזוזה בנתה מחדש את כל שכבת ה-SVG,
וזה מחק את האלמנט שהאצבע מחזיקה — הגרירה לא עבדה בשום מכשיר.
ההאזנה עברה ל-window, ולכן בנייה מחדש אינה מפריעה. בנוסף
touch-action:none על הידיות, גלילת המסגרת מושבתת בזמן גרירה,
ועובי הידית נגזר מגודל התצוגה כדי שיהיה ~44px של מסך גם בנייד.
מצב שני באותו מסך (סימון קטעים). שני חלונות זה לצד זה במחשב,
זה מעל זה בנייד — כי צריך לראות את שניהם בבת אחת כדי לתאם.
החברותא היא טקסט, לא תמונה. ה-PDF אינו סריקה ויש בו שכבת טקסט מלאה. טקסט מאפשר לחלון ההסבר להימתח לפי אורכו ולהישבר לפי רוחב המסך, ובעיקר — הסימון שם הוא בחירת טקסט רגילה.
כלל המילה המרחפת חל על דף וילנא בלבד. הוא הוחל בטעות גם על החברותא, ושם "א. מפתח של גשמים," הוא משפט לגיטימי שתופס חמישית מהשורה — ונמחק. משפטים קצרים נעלמו, וזה נראה כמגבלה של ה-PDF בעוד שהטקסט היה שלם.
ההדגשה מזוהה לפי גובה הגליף ולא לפי שם הגופן. pdf.js מחזיר
fontFamily גנרי לשני הגופנים, והשם האמיתי זמין רק אחרי ציור
העמוד — יקר מדי לעשרה עמודים. הגובה חד-משמעי: 12.96px לגוף
הטקסט, 15px למילים המודגשות, 18.96px לכותרות. הסף נגזר מהגובה
השכיח בפועל ולא מקבוע.
הרווחים מוסקים מהמרחק. ה-PDF אינו שומר רווחים בין קטעי
טקסט, ובלי זה המילים נדבקות. עברית נקראת מימין לשמאל, ולכן
הקטע הבא מתחיל שמאלה מקודמו והפער ביניהם הוא הרווח. נמדד:
רווח אמיתי ~2.9 יחידות, ובתוך מילה או בין ( ל-1 הפער 0 או
שלילי; הסף 0.15 מגובה הגליף נופל ביניהם.
בנייד, כל אינטראקציה מבוססת-גרירה מתנגשת עם גלילת המסך: בחירת טקסט בחברותא לא עבדה כלל, וגרירת ידית הזיזה את הדף. לכן כל סימון הוא הקשה, ואז אין במה להתנגש.
אין דילוגים בין קטע לקטע, ולכן ההתחלה תמיד ידועה: היכן שהקודם
נגמר — בנקודה, לא בשורה. קטע שנעצר באמצע שורה משאיר את חציה,
והחצי שייך לקטע הבא; nextStart() מחזיר את אותה נקודה עצמה,
ועובר לשורה הבאה רק אם הקודם מיצה אותה. שמירת מספר השורה בלבד
בלעה את החצי שנשאר. די בהקשה אחת על הסוף בכל צד. הקשה ארוכה קובעת התחלה
חדשה, ליוצא מן הכלל. השקף נשאר בתוקף עד שמשנים אותו — ▸
מקדם אותו באחת.
שני הצדדים סימטריים: לפעמים מסמנים קודם בחברותא ולפי זה בגמרא, ולכן אף צד אינו ראשי ואפשר להחליף ביניהם.
מספרי השורות הם ביחס לעמוד שעליו סומנו, ולכן רשימה אחת משותפת
לשני עמודי הדף היא נתון שבור: מעבר מעמוד א׳ לעמוד ב׳ השאיר את
הקטעים הקודמים ואת נקודת ההמשך שלהם, ושורה 5 של עמוד אחד
נראתה כשורה 5 של השני. BOOK מחזיק רשומה לכל עמוד, ומעבר
מחליף אותה.
הקובץ הנשמר נושא את זהותו — מסכת, דף, וכל העמודים שסומנו בהם —
ושמו daf-<מסכת>-<דף>.json. קודם הוא נקרא daf.json ולא היה בו
זכר לדף שממנו בא. ההודעה נוקבת במספר הקטעים, כי זה מה שמעניין
אחרי שעה של סימון.
שתי תקלות אמיתיות באו מכאן. מעבר לכיול וחזרה לסימון מחק את מה
שסומן: setMode('mark') קורא לגיליון, והגיליון דרס בזיכרון עמוד
שהעבודה בו טרם נשמרה. ומי שלא לחץ "שמירה" איבד שעה של עבודה, בלי
שדבר על המסך אמר לו שיש מה לאבד.
מקומית — כל שינוי נכתב מיד ל-localStorage (touched()). מיידי,
בחינם, בלי שרת ובלי מכסה, ומחזיר את העבודה גם אחרי סגירת
הכרטיסייה. בגיליון — נשאר מפורש, כי כתיבה לשם כותבת את
הלשונית כולה מחדש ואסור שהיא תרוץ בשקט תוך כדי קריאה ותתנגש
בעצמה. במקומה יש תג אדום בפס — "● N לא נשמרו" — ואזהרה לפני
סגירת הדף.
loadFromSheet() מדלג על מפתחות שב-DIRTY. בלי זה כל טעינה
מהגיליון מחזירה את הגרסה הישנה על גבי החדשה.
שעה של סימון שנמחקת בריענון היא נזק אמיתי, ולכן הכל נכתב
ללשונית "סימוני הדף" דרך אותו Apps Script — פעולת table
שכבר קיימת בו. שורה לכל עמוד: מסכת, דף, עמוד, ו-JSON של
האזורים והקטעים.
table כותב את הלשונית מחדש, ולכן חייבים למזג: קוראים קודם
את מה שכבר בגיליון (gviz), מחליפים רק את הדף שעבדנו עליו, וכותבים
הכל בחזרה. בלי זה, שמירה של דף אחד הייתה מוחקת את כל השאר.
הכניסה לסימון טוענת אוטומטית את מה שכבר סומן בדף הזה.
קטע יכול להיגמר בעמוד אחד ולהמשיך בבא, ולכן יש מעבר עמוד בפס התחתון — בלי לצאת ממסך הסימון ובלי לאבד את המצב. "→ חזרה לכיול" ו"סימון קטעים ←" מחליפים בין שני המסכים, וכל אחד שומר את שלו.
השורות מתאפסות במעבר עמוד, החברותא לא. מספרי השורות הם ביחס
לעמוד, ולכן עמוד חדש מתחיל משורה 0. קובץ החברותא לעומת זאת רציף
לשני העמודים, ולעיתים קרובות הפסקה שחוצה את המעבר היא בדיוק זו
של השורה הראשונה בעמוד הבא. איפוס המונה בכל מעבר גרם לכך שהקטע
הראשון של עמוד ב׳ בלע מחדש את כל פסקאות עמוד א׳ — בדף ב׳ יצאו 57
פסקאות על שורה אחת, ובלימוד הופיע ביאור שאינו של השורה שעל המסך.
lastChav() ממשיך מהעמוד הקודם של אותו דף.
החלפת דף בתוך מסך הסימון טוענת מחדש גם את החברותא וגם את
הסימונים מהגיליון. בלי זה נשארה החברותא של הדף הקודם לצד התמונה
החדשה, והדף החדש כלל לא נכנס ל-BOOK — כך שגם "שמירת JSON" הוציאה
את הדף הקודם.
סוגיה אינה עוצרת בתחתית עמוד א׳: משפט מתחיל שם ונגמר בראש עמוד ב׳,
והביאור אחד. הקטע נשמר כשני חלקים, אחד בכל עמוד, עם אותו
ביאור, והשני מסומן cont. כל חלק נשאר תקין ביחס לעמוד שלו —
מספרי השורות נשארים מקומיים ואין צורך לשנות דבר במודל — והלימוד
מציג את שניהם כקטע אחד: קודם הזנב שבסוף עמוד א׳, אחר כך הראש
שבעמוד ב׳, ורק אז מתקדם. המונה מראה "קטע 16 · חלק 1 מ-2".
בסטודיו: מסמנים את הזנב ומוסיפים כרגיל; עוברים עמוד, מסמנים את הראש, ולוחצים "↩ המשך הקטע" — בלי לבחור חברותא, כי היא כבר נבחרה. הכפתור פעיל רק בעמוד שעדיין אין בו קטעים ושלפניו יש עמוד מסומן.
וזה עובד גם על פני גבול הדף. סוגיה אינה עוצרת בסוף הדף יותר
משהיא עוצרת בסוף העמוד: ד׳ ע"ב ממשיך לה׳ ע"א. "הבא" ממשיך אל
הדף הבא כשנגמרים עמודיו של הנוכחי, "הקודם" נוחת על עמודו האחרון
של הדף שלפניו, ו-prevStep() מחפש את הקטע האחרון גם בדף הקודם.
הביאור נלקח אז מקובץ חברותא אחר — וזה תקין, כי הטקסט נשמר בקטע
עצמו.
בלי זה הלימוד היה קופץ מסוף עמוד א׳ לתחילת ב׳, והתלמיד לא היה רואה כלל את החצי הראשון של הקטע.
הזום משנה את הרוחב בפועל ולא רק את הטרנספורם. עם
transform: scale() תיבת הפריסה נשארת בגודלה, ולכן ל-.stage לא
היה מה לגלול: התזוזה עברה דרך PANX/PANY של הפינץ' בלבד והגיעה עד
גבול שרירותי — התמונה נראתה תקועה ואי אפשר היה למשוך אותה למטה
כדי לראות את השורות שמעל. עכשיו זו גלילה רגילה של הדפדפן, בשני
הצירים, באצבע אחת. הפינץ' נשאר לזום בלבד.
היו שם שנים-עשר כפתורים בשתי שורות שנשברו לארבע, ותפסו שליש ממסך הטלפון בזמן שהעבודה עצמה — הדף והחברותא — נדחסה למה שנשאר. רובם גם לא היו רלוונטיים לרגע: מעבר עמוד פעמיים בדף, שמירה בסוף, החלפת צדדים כמעט אף פעם.
עכשיו בפס יש רק מה שנוגע לקטע שמסמנים כרגע — ⋯ · ביטול · כוונון הסוף · ניקוי · והפעולה הראשית — וכל השאר בתפריט ⋯. גובה הפס 56px במקום כ-200.
כפתור ראשי אחד שיודע מה הוא. בראש עמוד חדש, אחרי שהעמוד הקודם סומן ולפני שנבחרה פסקה, הוא מציע "↩ המשך הקטע"; ברגע שנבחרת פסקה הוא חוזר להיות "הוספת הקטע". שני כפתורים זה לצד זה רק הכריחו לבחור בין שניים שרק אחד מהם נכון.
הכפתורים לכוונון תחילת הקטע ירדו. ההתחלה תמיד ממשיכה מהמקום שהקודם נעצר בו, ולא היה בהם צורך מעולם. נשאר כוונון הסוף בלבד, והוא מופיע רק כשיש סימון.
הגבהים היו חשבון פיקסלים — calc(100vh - 168px) — והוא נשבר בכל
פעם שהכותרת נשברה לשתי שורות או שהודעה התארכה. עכשיו body.mark
הוא flex אנכי בגובה 100dvh: כותרת · שני החלונות שמתחלקים בכל מה
שנשאר · פס. שים לב ש-.wrap הוא align-items:start בכיול, ובסימון
הוא חייב align-items:stretch — בלעדיו החלונות נשארים בגובה תוכנם
והחברותא נמתחת לארבעת אלפים פיקסלים מחוץ למסך.
צוירו רק הבחירה הנוכחית וקו דק במקום שממנו ימשיך הקטע הבא, ולכן עמוד שכבר סומן נראה בדיוק כמו עמוד ריק. זה מה שנראה כתקלה במעבר עמוד: הרכז עבר לעמוד ב׳ שכבר היו בו עשרים קטעים מהגיליון, ראה דף נקי, הקיש על השורה השנייה — והמערכת סימנה אותה לבדה, כי נקודת ההמשך האמיתית הייתה בשורה 45. גם החברותא נראתה כולה מעומעמת, כי כל 125 הפסקאות באמת כבר שובצו.
הכל היה נכון, ובלתי נראה. הקטעים מצוירים עכשיו בירוק עמום בשני גוונים לסירוגין, הפס אומר כמה קטעים יש בעמוד, ועמוד שסומן עד סופו אומר זאת במילים.
הדף מתגלגל אל השורה הבאה והחברותא אל הפסקה הבאה, אוטומטית — רק כשהן אינן נראות ממילא, שלא יזוז דבר מתחת לאצבע. בלי זה כל קטע דרש שתי גלילות ידניות ורק אז הקשה אחת.
מקביל — חצי מסך לכל צד, לעבודה מיושבת. מסך מלא — צד אחד בכל פעם, להליכה ולעיניים עייפות. אותה לוגיקה בשתיהן; רק הפריסה משתנה.
הקטע שאליו מתייחסים לא תמיד מתחיל בתחילת שורה ולא תמיד נגמר
בסופה, ולכן גבול הוא (שורה, מיקום). x הוא שבר מרוחב השורה,
0 = הקצה הימני. ההדגשה נראית כמו בחירת טקסט: מהנקודה עד קצה
השורה, שורות מלאות באמצע, ובאחרונה עד הנקודה — selShapes().
שתי ידיות זהובות מכווננות את הקצוות בתוך השורה.
daf/מסך התלמידים אינו נשען על ה-Apps Script. הסקריפט מוגבל בריצות מקבילות, כל בקשה קוראת PDF של מגבייט ומקודדת אותו, והכל רץ על חשבון גוגל אחד — שיעור שבו שלושים תלמידים פותחים את הדף באותה דקה נתקע. תמונה סטטית מ-CDN היא הפוכה בכל פרמטר: בלי מכסה, בלי צוואר, עובדת אופליין, ומהירה בהרבה — 350KB במקום PDF של 2MB שצריך לצייר בדפדפן.
python3 tools/build-library.py # הכול
python3 tools/build-library.py taanit ב # דף אחד
git add daf && git commit && git push
הכלי רץ אצל הרכז, כי הוא מושך את הקבצים דרך הסקריפט שלו.
הפלט: daf/<מסכת>/<דף>-a.webp, -b.webp ו-daf/index.json.
המפתח הוא אות הדף בלי גרשיים, כמו dafKey שבאפליקציה.
רק צורת הדף. פירוש חברותא אינו מומר לתמונות — הביאור מגיע כטקסט מתוך הסימונים שבגיליון.
learn.html בודק את daf/index.json ומעדיף אותו; דף שטרם הומר
ממשיך לעבוד דרך הדרייב, ולכן אפשר להמיר בהדרגה. הפרוקסי נשאר
לסטודיו, שם יש משתמש אחד וקבצים פרטיים.
ההדגשה נשמרת בסימון. c.h מחזיק את פסקאות החברותא עם
תגי <b>, ולכן הלימוד אינו צריך את קובץ החברותא כלל. סימונים
שנשמרו לפני כן עדיין מושכים אותו — needChav() בודק, וטוען רק
אם באמת חסר.
/learnהמסך שהתלמיד רואה. הוא קורא את מה שסומן בסטודיו ומציג את הדף
קטע אחר קטע: learn.html?mas=taanit&daf=ב. בלי פרמטרים הוא לוקח
את הדף של השבוע הנוכחי מלוח השנה.
הכניסה אליו היא כפתור בעמוד השבוע, והוא מופיע רק לדף שכבר סומן
— loadMarked() שואל את הגיליון את שתי העמודות הראשונות בלבד
(select A,B), כי הנתונים עצמם כבדים ולעמוד השבוע די לדעת אילו
דפים מוכנים. כפתור שמבטיח לימוד ומגיע למסך ריק גרוע מכפתור שאינו
קיים.
loadMarks() מבקש מהגיליון את הדף שעל המסך ואת הבא אחריו בלבד.
הבא נחוץ כי קטע יכול להתחיל בתחתית הדף הזה ולהיגמר בראש הבא, והזנב
נשמר שם; בלעדיו התלמיד רואה רק את חציו הראשון.
קודם נשלפה כל הלשונית והסינון נעשה במכשיר — כלומר שלושים דפים ירדו כדי להציג אחד. זה עבד, אבל גדל עם ההצלחה: 220KB ב-17 עמודים מסומנים, וכמיליון וחצי בייט בתוכנית מלאה — בכל פתיחה, אצל כל תלמיד, בלי מטמון (הבקשה נושאת חותמת זמן בכוונה, כדי שסימון חדש יגיע מיד). עכשיו זה ~52KB קבוע, בלי קשר לכמה דפים סומנו.
שני פרטים שאסור לשבור:
כ' מצד אחד, י"ט מצד שני —
ולשפת השאילתה של הגיליון אין תו בריחה. לכן כל ערך עוטף את עצמו
בסימן שאינו מופיע בתוכו, ונשלחות שתי הצורות, עם הפיסוק ובלעדיו,
כדי שדבר לא ייתלה בניחוש איך הסטודיו כתב את השם.ואם השאילתה חזרה ריקה — חוזרים לשליפה המלאה. איטי, אבל נכון: תקלה כאן אינה נראית כשגיאה אלא כ"הדף הזה עוד לא הוכן ללימוד".
לא כרטיסיות ולא טקסט מועתק. התלמיד רואה את צורת הדף האמיתית — אותה שלפניו בגמרא — ולומד למצוא בה את מקומו. מה שמשתנה הוא רק מה מואר.
שאר הדף אינו נעלם, הוא מעומעם: מלבן בצבע הנייר על כל העמוד,
ובתוכו חורים בצורת הקטע (<mask>). כך הדיו של הקטע נשאר הדיו
המקורי ולא נצבע — מה שחשוב בגמרא, שכל האותיות בה שחורות.
אותה נקודה משמשת פעמיים: היא הסוף של קטע אחד וההתחלה של הבא, והיא יושבת בגבול שבין המילה שלנו לשכנה — נוטה מעט אל צד השכנה. לכן שני הגבולות זזים לאותו כיוון, שמאלה, בשישית מגובה השורה: בהתחלה זה מוריד את זנב המילה הקודמת, ובסוף זה נותן אוויר לאות האחרונה. הזזה לכיוון ההפוך בסוף — כלומר "פנימה" משני הצדדים — חנקה את האות האחרונה של כל קטע. ומכיוון ששניהם זזים יחד, קטע וקטע נשארים צמודים בדיוק.
בטלפון אי אפשר להראות דף וילנה שלם וגם לקרוא אותו, ולכן החלון נע: הזום נקבע לפי הקטע הנוכחי, והמעבר בין קטעים הוא תנועה על הדף.
רוחב החלון הוא הטור שבו הקטע יושב — לא הדף ולא הגוש המרכזי. כאן זה נופל או עומד: השדרה תופסת כשליש מרוחב הדף, וחלון ברוחב הגוש כולו נתן הגדלה של 1.19 בלבד והשאיר את הגמרא זעירה. ברוחב הטור ההגדלה כמעט 3, הגמרא ממלאת את המסך, ושולי הטורים השכנים עדיין מציצים מהצדדים. גובה החלון הוא הקטע ועוד כשתי שורות מכל צד, ולפחות שבע שורות — כדי שקטע של שורה אחת לא ייראה כמו כרזה.
כפתור "כל הדף" מקטין לדף כולו, והקטע הנוכחי נשאר מואר. שם ההקשה על הדף כבויה: התלמיד מסתכל סביב, ולא היה נעים שכל מבט יקפיץ אותו קדימה.
לא כפתור לכל קטע. מי שפתח אותה — היא רצה איתו עד סוף הדף, ומי שלא — המסך שלם בלעדיה. בכל קטע כבר שמור הטקסט שסומן לו, ולכן המגירה עובדת מיד. אבל בקובץ המקורי המילים הפותחות מודגשות, והן בדיוק מה שמאפשר לתלמיד למצוא את עצמו — ולכן בפתיחה הראשונה נטען הקובץ ברקע וההדגשה משוחזרת לפי אינדקסי הפסקאות. לפני שסומכים על האינדקסים משווים את הטקסט למה שנשמר; לא זהה — נשארים עם השמור, בלי הודעה.
"ישר כוח!", ומשם קישור לשאלה השבועית — /#quiz=taanit-1, האש
שהאפליקציה יודעת לנחות עליו ישירות על החידה של אותו שבוע.