feat(rsvp): add persistent context, display settings, and layout improvements, closes #3333 (#3528)

* feat(rsvp): add persistent context, display settings, and layout improvements, closes #3333

Add always-visible collapsible context panel, font size adjustment,
ORP color selection, and stable context window that only rebuilds on
scroll. Refactor speed controls into playback row with collapsible
settings behind a gear icon. Translate new i18n keys across 29 locales.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(selfhost): update docker db schema to match FileRecords (#3527)

In #2636, FileRecord was defined to have updated_at  field
which is used when it is accessed from the database. But the
local dev setup is missing this field.

This diff adds an updated_at column to match the expectation

* chore(ci): cache system dependency and rustc outputs in ci

* chore(ci): lint script changed from tsc to tsgo

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Azeem Bande-Ali <me@azeemba.com>
This commit is contained in:
Huang Xin
2026-03-13 13:57:47 +08:00
committed by GitHub
parent 34ad6e6749
commit d06e1849cc
37 changed files with 620 additions and 219 deletions
+20
View File
@@ -12,16 +12,25 @@ jobs:
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C target-cpu=skylake'
SCCACHE_GHA_ENABLED: 'true'
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@v6
with:
submodules: 'true'
- name: setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Install minimal stable with clippy and rustfmt
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Cache apt packages
uses: actions/cache@v5
with:
path: /var/cache/apt/archives
key: apt-rust-lint-${{ runner.os }}
- name: Install system dependencies
run: |
sudo apt-get update
@@ -87,6 +96,9 @@ jobs:
build_tauri_app:
runs-on: ubuntu-latest
env:
SCCACHE_GHA_ENABLED: 'true'
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@v6
with:
@@ -115,6 +127,9 @@ jobs:
run: |
pnpm install && pnpm setup-vendors
- name: setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
@@ -125,6 +140,11 @@ jobs:
key: tauri-cargo
cache-all-crates: 'true'
- name: Cache apt packages
uses: actions/cache@v5
with:
path: /var/cache/apt/archives
key: apt-tauri-${{ runner.os }}
- name: install system dependencies
run: |
sudo apt-get update
+3
View File
@@ -48,6 +48,9 @@ tauri.*.conf.json
*.tsbuildinfo
next-env.d.ts
# eslint
.eslintcache
#generated
src-tauri/gen
+4 -3
View File
@@ -10,17 +10,17 @@
"dev-web": "dotenv -e .env.web -- next dev",
"build-web": "dotenv -e .env.web -- next build",
"start-web": "dotenv -e .env.web -- next start",
"dev-web:vinext": "dotenv -e .env.web -- vinext dev --port 3000",
"dev-web:vinext": "dotenv -e .env.web -- vinext dev",
"build-web:vinext": "dotenv -e .env.web -- vinext build",
"start-web:vinext": "dotenv -e .env.web -- vinext start",
"build-tauri": "dotenv -e .env.tauri -- next build",
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
"lint": "tsc --noEmit && eslint .",
"lint": "tsgo --noEmit && eslint --cache .",
"test": "dotenv -e .env -e .env.test.local vitest",
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
"test:tauri": "bash scripts/test-tauri.sh",
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser",
"test:pr:tauri": "pnpm test -- --watch=false && bash scripts/test-tauri.sh",
"test:pr:tauri": "bash scripts/test-tauri.sh",
"test:all": "pnpm test -- --watch=false && pnpm test:browser && bash scripts/test-tauri.sh",
"test:e2e": "wdio run wdio.conf.ts",
"tauri:dev:test": "dotenv -e .env.tauri -- tauri dev --features webdriver",
@@ -194,6 +194,7 @@
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"@typescript/native-preview": "7.0.0-dev.20260312.1",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-rsc": "^0.5.21",
"@vitest/browser-playwright": "^4.0.18",
@@ -1038,10 +1038,8 @@
"Play (Space)": "تشغيل (المسافة)",
"Skip forward 15 words": "تخطي للأمام 15 كلمة",
"Forward 15 words (Shift+Right)": "للأمام 15 كلمة (Shift+اليمين)",
"Pause:": "إيقاف مؤقت:",
"Decrease speed": "تقليل السرعة",
"Slower (Left/Down)": "أبطأ (اليسار/الأسفل)",
"Current speed": "السرعة الحالية",
"Increase speed": "زيادة السرعة",
"Faster (Right/Up)": "أسرع (اليمين/الأعلى)",
"Start RSVP Reading": "بدء قراءة RSVP",
@@ -1113,5 +1111,11 @@
"Translating...": "جارٍ الترجمة...",
"Show Battery Percentage": "عرض نسبة البطارية",
"Hide Scrollbar": "إخفاء شريط التمرير",
"Skip to last reading position": "انتقل إلى آخر موضع قراءة"
"Skip to last reading position": "انتقل إلى آخر موضع قراءة",
"Show context": "إظهار السياق",
"Hide context": "إخفاء السياق",
"Decrease font size": "تصغير حجم الخط",
"Increase font size": "تكبير حجم الخط",
"Focus": "تركيز",
"Theme color": "لون السمة"
}
@@ -990,10 +990,8 @@
"Play (Space)": "চালান (Space)",
"Skip forward 15 words": "১৫ শব্দ এগিয়ে যান",
"Forward 15 words (Shift+Right)": "১৫ শব্দ এগিয়ে যান (Shift+Right)",
"Pause:": "বিরতি:",
"Decrease speed": "গতি কমান",
"Slower (Left/Down)": "ধীর (Left/Down)",
"Current speed": "বর্তমান গতি",
"Increase speed": "গতি বাড়ান",
"Faster (Right/Up)": "দ্রুত (Right/Up)",
"Start RSVP Reading": "RSVP পড়া শুরু করুন",
@@ -1065,5 +1063,11 @@
"Translating...": "অনুবাদ করা হচ্ছে...",
"Show Battery Percentage": "ব্যাটারি শতাংশ দেখান",
"Hide Scrollbar": "স্ক্রলবার লুকান",
"Skip to last reading position": "শেষ পড়ার অবস্থানে যান"
"Skip to last reading position": "শেষ পড়ার অবস্থানে যান",
"Show context": "প্রসঙ্গ দেখান",
"Hide context": "প্রসঙ্গ লুকান",
"Decrease font size": "ফন্টের আকার কমান",
"Increase font size": "ফন্টের আকার বাড়ান",
"Focus": "ফোকাস",
"Theme color": "থিম রঙ"
}
@@ -978,10 +978,8 @@
"Play (Space)": "གཏོང་བ། (Space)",
"Skip forward 15 words": "ཚིག་ ༡༥ མདུན་ལ་བཤུད་པ།",
"Forward 15 words (Shift+Right)": "ཚིག་ ༡༥ མདུན་ལ་བཤུད་པ། (Shift+Right)",
"Pause:": "མཚམས་འཇོག་པ།",
"Decrease speed": "མགྱོགས་ཚད་འགོར་དུ་གཏོང་བ།",
"Slower (Left/Down)": "འགོར་བ། (Left/Down)",
"Current speed": "ད་ལྟའི་མགྱོགས་ཚད།",
"Increase speed": "མགྱོགས་ཚད་མྱུར་དུ་གཏོང་བ།",
"Faster (Right/Up)": "མྱུར་བ། (Right/Up)",
"Start RSVP Reading": "RSVP ཀློག་འགོ་འཛུགས་པ།",
@@ -1053,5 +1051,11 @@
"Translating...": "ཡིག་བསྒྱུར་བྱེད་བཞིན་པ།...",
"Show Battery Percentage": "གློག་རྫས་བརྒྱ་ཆ་སྟོན།",
"Hide Scrollbar": "འགྲིལ་ཤིང་སྦེད་པ།",
"Skip to last reading position": "མཐའ་མའི་ཀློག་གནས་སུ་མཆོང་"
"Skip to last reading position": "མཐའ་མའི་ཀློག་གནས་སུ་མཆོང་",
"Show context": "སྐབས་དོན་མངོན་པ",
"Hide context": "སྐབས་དོན་སྦས་པ",
"Decrease font size": "ཡིག་གཟུགས་ཆུང་དུ་གཏོང་བ",
"Increase font size": "ཡིག་གཟུགས་ཆེ་རུ་གཏོང་བ",
"Focus": "དམིགས་གཏད",
"Theme color": "བརྗོད་དོན་ཚོན་མདོག"
}
@@ -990,10 +990,8 @@
"Play (Space)": "Abspielen (Leertaste)",
"Skip forward 15 words": "15 Wörter vor",
"Forward 15 words (Shift+Right)": "15 Wörter vor (Shift+Rechts)",
"Pause:": "Pause:",
"Decrease speed": "Geschwindigkeit verringern",
"Slower (Left/Down)": "Langsamer (Links/Unten)",
"Current speed": "Aktuelle Geschwindigkeit",
"Increase speed": "Geschwindigkeit erhöhen",
"Faster (Right/Up)": "Schneller (Rechts/Oben)",
"Start RSVP Reading": "RSVP-Lesen starten",
@@ -1065,5 +1063,11 @@
"Translating...": "Übersetzen...",
"Show Battery Percentage": "Akkustand in Prozent anzeigen",
"Hide Scrollbar": "Scrollleiste ausblenden",
"Skip to last reading position": "Zur letzten Leseposition springen"
"Skip to last reading position": "Zur letzten Leseposition springen",
"Show context": "Kontext anzeigen",
"Hide context": "Kontext ausblenden",
"Decrease font size": "Schriftgröße verkleinern",
"Increase font size": "Schriftgröße vergrößern",
"Focus": "Fokus",
"Theme color": "Themenfarbe"
}
@@ -990,10 +990,8 @@
"Play (Space)": "Αναπαραγωγή (Διάστημα)",
"Skip forward 15 words": "Προώθηση 15 λέξεων",
"Forward 15 words (Shift+Right)": "Εμπρός 15 λέξεις (Shift+Δεξιά)",
"Pause:": "Παύση:",
"Decrease speed": "Μείωση ταχύτητας",
"Slower (Left/Down)": "Πιο αργά (Αριστερά/Κάτω)",
"Current speed": "Τρέχουσα ταχύτητα",
"Increase speed": "Αύξηση ταχύτητας",
"Faster (Right/Up)": "Πιο γρήγορα (Δεξιά/Πάνω)",
"Start RSVP Reading": "Έναρξη ανάγνωσης RSVP",
@@ -1065,5 +1063,11 @@
"Translating...": "Μετάφραση...",
"Show Battery Percentage": "Εμφάνιση ποσοστού μπαταρίας",
"Hide Scrollbar": "Απόκρυψη γραμμής κύλισης",
"Skip to last reading position": "Μετάβαση στην τελευταία θέση ανάγνωσης"
"Skip to last reading position": "Μετάβαση στην τελευταία θέση ανάγνωσης",
"Show context": "Εμφάνιση πλαισίου",
"Hide context": "Απόκρυψη πλαισίου",
"Decrease font size": "Μείωση μεγέθους γραμματοσειράς",
"Increase font size": "Αύξηση μεγέθους γραμματοσειράς",
"Focus": "Εστίαση",
"Theme color": "Χρώμα θέματος"
}
@@ -1002,10 +1002,8 @@
"Play (Space)": "Reproducir (Espacio)",
"Skip forward 15 words": "Adelantar 15 palabras",
"Forward 15 words (Shift+Right)": "Adelantar 15 palabras (Shift+Derecha)",
"Pause:": "Pausa:",
"Decrease speed": "Disminuir velocidad",
"Slower (Left/Down)": "Más lento (Izquierda/Abajo)",
"Current speed": "Velocidad actual",
"Increase speed": "Aumentar velocidad",
"Faster (Right/Up)": "Más rápido (Derecha/Arriba)",
"Start RSVP Reading": "Iniciar lectura RSVP",
@@ -1077,5 +1075,11 @@
"Translating...": "Traduciendo...",
"Show Battery Percentage": "Mostrar porcentaje de batería",
"Hide Scrollbar": "Ocultar barra de desplazamiento",
"Skip to last reading position": "Ir a la última posición de lectura"
"Skip to last reading position": "Ir a la última posición de lectura",
"Show context": "Mostrar contexto",
"Hide context": "Ocultar contexto",
"Decrease font size": "Reducir tamaño de fuente",
"Increase font size": "Aumentar tamaño de fuente",
"Focus": "Enfoque",
"Theme color": "Color del tema"
}
@@ -990,10 +990,8 @@
"Play (Space)": "پخش (Space)",
"Skip forward 15 words": "۱۵ کلمه به جلو",
"Forward 15 words (Shift+Right)": "۱۵ کلمه به جلو (Shift+Right)",
"Pause:": "توقف:",
"Decrease speed": "کاهش سرعت",
"Slower (Left/Down)": "کندتر (Left/Down)",
"Current speed": "سرعت فعلی",
"Increase speed": "افزایش سرعت",
"Faster (Right/Up)": "سریع‌تر (Right/Up)",
"Start RSVP Reading": "شروع مطالعه RSVP",
@@ -1065,5 +1063,11 @@
"Translating...": "در حال ترجمه...",
"Show Battery Percentage": "نمایش درصد باتری",
"Hide Scrollbar": "مخفی کردن نوار پیمایش",
"Skip to last reading position": "رفتن به آخرین موقعیت مطالعه"
"Skip to last reading position": "رفتن به آخرین موقعیت مطالعه",
"Show context": "نمایش زمینه",
"Hide context": "پنهان کردن زمینه",
"Decrease font size": "کاهش اندازه قلم",
"Increase font size": "افزایش اندازه قلم",
"Focus": "تمرکز",
"Theme color": "رنگ پوسته"
}
@@ -1002,10 +1002,8 @@
"Play (Space)": "Lecture (Espace)",
"Skip forward 15 words": "Avancer de 15 mots",
"Forward 15 words (Shift+Right)": "Avancer de 15 mots (Shift+Right)",
"Pause:": "Pause :",
"Decrease speed": "Diminuer la vitesse",
"Slower (Left/Down)": "Plus lent (Gauche/Bas)",
"Current speed": "Vitesse actuelle",
"Increase speed": "Augmenter la vitesse",
"Faster (Right/Up)": "Plus rapide (Droite/Haut)",
"Start RSVP Reading": "Démarrer la lecture RSVP",
@@ -1077,5 +1075,11 @@
"Translating...": "Traduction en cours...",
"Show Battery Percentage": "Afficher le pourcentage de batterie",
"Hide Scrollbar": "Masquer la barre de défilement",
"Skip to last reading position": "Aller à la dernière position de lecture"
"Skip to last reading position": "Aller à la dernière position de lecture",
"Show context": "Afficher le contexte",
"Hide context": "Masquer le contexte",
"Decrease font size": "Réduire la taille de police",
"Increase font size": "Augmenter la taille de police",
"Focus": "Focus",
"Theme color": "Couleur du thème"
}
@@ -502,10 +502,8 @@
"Play (Space)": "נגן (רווח)",
"Skip forward 15 words": "קפוץ 15 מילים קדימה",
"Forward 15 words (Shift+Right)": "15 מילים קדימה (Shift+Right)",
"Pause:": "השהה:",
"Decrease speed": "הפחת מהירות",
"Slower (Left/Down)": "איטי יותר (חץ שמאלה/למטה)",
"Current speed": "מהירות נוכחית",
"Increase speed": "הגבר מהירות",
"Faster (Right/Up)": "מהיר יותר (חץ ימינה/למעלה)",
"Start RSVP Reading": "התחל קריאת RSVP",
@@ -1077,5 +1075,11 @@
"Translating...": "מתרגם...",
"Show Battery Percentage": "הצג אחוזי סוללה",
"Hide Scrollbar": "הסתר סרגל גלילה",
"Skip to last reading position": "דלג למיקום הקריאה האחרון"
"Skip to last reading position": "דלג למיקום הקריאה האחרון",
"Show context": "הצג הקשר",
"Hide context": "הסתר הקשר",
"Decrease font size": "הקטן גודל גופן",
"Increase font size": "הגדל גודל גופן",
"Focus": "מיקוד",
"Theme color": "צבע ערכת נושא"
}
@@ -990,10 +990,8 @@
"Play (Space)": "चलाएं (Space)",
"Skip forward 15 words": "15 शब्द आगे",
"Forward 15 words (Shift+Right)": "15 शब्द आगे (Shift+Right)",
"Pause:": "विराम:",
"Decrease speed": "गति कम करें",
"Slower (Left/Down)": "धीमी (Left/Down)",
"Current speed": "वर्तमान गति",
"Increase speed": "गति बढ़ाएं",
"Faster (Right/Up)": "तेज़ (Right/Up)",
"Start RSVP Reading": "RSVP पठन शुरू करें",
@@ -1065,5 +1063,11 @@
"Translating...": "अनुवाद हो रहा है...",
"Show Battery Percentage": "बैटरी प्रतिशत दिखाएं",
"Hide Scrollbar": "स्क्रॉलबार छुपाएँ",
"Skip to last reading position": "अंतिम पढ़ने की स्थिति पर जाएँ"
"Skip to last reading position": "अंतिम पढ़ने की स्थिति पर जाएँ",
"Show context": "संदर्भ दिखाएँ",
"Hide context": "संदर्भ छुपाएँ",
"Decrease font size": "फ़ॉन्ट आकार घटाएँ",
"Increase font size": "फ़ॉन्ट आकार बढ़ाएँ",
"Focus": "फ़ोकस",
"Theme color": "थीम रंग"
}
@@ -978,10 +978,8 @@
"Play (Space)": "Putar (Spasi)",
"Skip forward 15 words": "Lompat maju 15 kata",
"Forward 15 words (Shift+Right)": "Maju 15 kata (Shift+Kanan)",
"Pause:": "Jeda:",
"Decrease speed": "Kurangi kecepatan",
"Slower (Left/Down)": "Lebih lambat (Kiri/Bawah)",
"Current speed": "Kecepatan saat ini",
"Increase speed": "Tambah kecepatan",
"Faster (Right/Up)": "Lebih cepat (Kanan/Atas)",
"Start RSVP Reading": "Mulai Membaca RSVP",
@@ -1053,5 +1051,11 @@
"Translating...": "Menerjemahkan...",
"Show Battery Percentage": "Tampilkan Persentase Baterai",
"Hide Scrollbar": "Sembunyikan Bilah Gulir",
"Skip to last reading position": "Lompat ke posisi baca terakhir"
"Skip to last reading position": "Lompat ke posisi baca terakhir",
"Show context": "Tampilkan konteks",
"Hide context": "Sembunyikan konteks",
"Decrease font size": "Perkecil ukuran font",
"Increase font size": "Perbesar ukuran font",
"Focus": "Fokus",
"Theme color": "Warna tema"
}
@@ -1002,10 +1002,8 @@
"Play (Space)": "Riproduci (Spazio)",
"Skip forward 15 words": "Avanti di 15 parole",
"Forward 15 words (Shift+Right)": "Avanti di 15 parole (Shift+Destra)",
"Pause:": "Pausa:",
"Decrease speed": "Diminuisci velocità",
"Slower (Left/Down)": "Più lento (Sinistra/Giù)",
"Current speed": "Velocità attuale",
"Increase speed": "Aumenta velocità",
"Faster (Right/Up)": "Più veloce (Destra/Su)",
"Start RSVP Reading": "Avvia lettura RSVP",
@@ -1077,5 +1075,11 @@
"Translating...": "Traduzione...",
"Show Battery Percentage": "Mostra percentuale batteria",
"Hide Scrollbar": "Nascondi barra di scorrimento",
"Skip to last reading position": "Vai all'ultima posizione di lettura"
"Skip to last reading position": "Vai all'ultima posizione di lettura",
"Show context": "Mostra contesto",
"Hide context": "Nascondi contesto",
"Decrease font size": "Riduci dimensione carattere",
"Increase font size": "Aumenta dimensione carattere",
"Focus": "Messa a fuoco",
"Theme color": "Colore del tema"
}
@@ -978,10 +978,8 @@
"Play (Space)": "再生 (Space)",
"Skip forward 15 words": "15単語進む",
"Forward 15 words (Shift+Right)": "15単語進む (Shift+右)",
"Pause:": "一時停止:",
"Decrease speed": "速度を下げる",
"Slower (Left/Down)": "低速 (左/下)",
"Current speed": "現在の速度",
"Increase speed": "速度を上げる",
"Faster (Right/Up)": "高速 (右/上)",
"Start RSVP Reading": "RSVP読書を開始",
@@ -1053,5 +1051,11 @@
"Translating...": "翻訳中...",
"Show Battery Percentage": "バッテリー残量を表示",
"Hide Scrollbar": "スクロールバーを非表示",
"Skip to last reading position": "最後の読書位置へ移動"
"Skip to last reading position": "最後の読書位置へ移動",
"Show context": "コンテキストを表示",
"Hide context": "コンテキストを非表示",
"Decrease font size": "文字サイズを縮小",
"Increase font size": "文字サイズを拡大",
"Focus": "フォーカス",
"Theme color": "テーマカラー"
}
@@ -978,10 +978,8 @@
"Play (Space)": "재생 (스페이스바)",
"Skip forward 15 words": "15단어 앞으로",
"Forward 15 words (Shift+Right)": "15단어 앞으로 (Shift+오른쪽 화살표)",
"Pause:": "일시정지:",
"Decrease speed": "속도 줄이기",
"Slower (Left/Down)": "느리게 (왼쪽/아래 화살표)",
"Current speed": "현재 속도",
"Increase speed": "속도 높이기",
"Faster (Right/Up)": "빠르게 (오른쪽/위 화살표)",
"Start RSVP Reading": "RSVP 독서 시작",
@@ -1053,5 +1051,11 @@
"Translating...": "번역 중...",
"Show Battery Percentage": "배터리 백분율 표시",
"Hide Scrollbar": "스크롤바 숨기기",
"Skip to last reading position": "마지막 읽기 위치로 이동"
"Skip to last reading position": "마지막 읽기 위치로 이동",
"Show context": "맥락 표시",
"Hide context": "맥락 숨기기",
"Decrease font size": "글꼴 크기 줄이기",
"Increase font size": "글꼴 크기 늘리기",
"Focus": "초점",
"Theme color": "테마 색상"
}
@@ -978,10 +978,8 @@
"Play (Space)": "Main (Ruang)",
"Skip forward 15 words": "Langkau depan 15 perkataan",
"Forward 15 words (Shift+Right)": "Depan 15 perkataan (Shift+Kanan)",
"Pause:": "Jeda:",
"Decrease speed": "Kurangkan kelajuan",
"Slower (Left/Down)": "Lebih perlahan (Kiri/Bawah)",
"Current speed": "Kelajuan semasa",
"Increase speed": "Tingkatkan kelajuan",
"Faster (Right/Up)": "Lebih cepat (Kanan/Atas)",
"Start RSVP Reading": "Mulakan Pembacaan RSVP",
@@ -1053,5 +1051,11 @@
"Translating...": "Menterjemah...",
"Show Battery Percentage": "Tunjukkan Peratusan Bateri",
"Hide Scrollbar": "Sembunyikan Bar Tatal",
"Skip to last reading position": "Lompat ke posisi bacaan terakhir"
"Skip to last reading position": "Lompat ke posisi bacaan terakhir",
"Show context": "Tunjukkan konteks",
"Hide context": "Sembunyikan konteks",
"Decrease font size": "Kecilkan saiz fon",
"Increase font size": "Besarkan saiz fon",
"Focus": "Fokus",
"Theme color": "Warna tema"
}
@@ -990,10 +990,8 @@
"Play (Space)": "Afspelen (Spatie)",
"Skip forward 15 words": "15 woorden vooruit",
"Forward 15 words (Shift+Right)": "15 woorden vooruit (Shift+Rechts)",
"Pause:": "Pauze:",
"Decrease speed": "Snelheid verlagen",
"Slower (Left/Down)": "Langzamer (Links/Omlaag)",
"Current speed": "Huidige snelheid",
"Increase speed": "Snelheid verhogen",
"Faster (Right/Up)": "Sneller (Rechts/Omhoog)",
"Start RSVP Reading": "Start RSVP-lezen",
@@ -1065,5 +1063,11 @@
"Translating...": "Vertalen...",
"Show Battery Percentage": "Batterijpercentage weergeven",
"Hide Scrollbar": "Schuifbalk verbergen",
"Skip to last reading position": "Ga naar laatste leespositie"
"Skip to last reading position": "Ga naar laatste leespositie",
"Show context": "Context tonen",
"Hide context": "Context verbergen",
"Decrease font size": "Lettergrootte verkleinen",
"Increase font size": "Lettergrootte vergroten",
"Focus": "Focus",
"Theme color": "Themakleur"
}
@@ -1014,10 +1014,8 @@
"Play (Space)": "Odtwarzaj (Spacja)",
"Skip forward 15 words": "Naprzód o 15 słów",
"Forward 15 words (Shift+Right)": "Naprzód o 15 słów (Shift+Prawo)",
"Pause:": "Pauza:",
"Decrease speed": "Zmniejsz prędkość",
"Slower (Left/Down)": "Wolniej (Lewo/Dół)",
"Current speed": "Aktualna prędkość",
"Increase speed": "Zwiększ prędkość",
"Faster (Right/Up)": "Szybciej (Prawo/Góra)",
"Start RSVP Reading": "Uruchom czytanie RSVP",
@@ -1089,5 +1087,11 @@
"Translating...": "Tłumaczenie...",
"Show Battery Percentage": "Pokaż procent baterii",
"Hide Scrollbar": "Ukryj pasek przewijania",
"Skip to last reading position": "Przejdź do ostatniej pozycji czytania"
"Skip to last reading position": "Przejdź do ostatniej pozycji czytania",
"Show context": "Pokaż kontekst",
"Hide context": "Ukryj kontekst",
"Decrease font size": "Zmniejsz rozmiar czcionki",
"Increase font size": "Zwiększ rozmiar czcionki",
"Focus": "Fokus",
"Theme color": "Kolor motywu"
}
@@ -1002,10 +1002,8 @@
"Play (Space)": "Reproduzir (Espaço)",
"Skip forward 15 words": "Avançar 15 palavras",
"Forward 15 words (Shift+Right)": "Avançar 15 palavras (Shift+Direita)",
"Pause:": "Pausa:",
"Decrease speed": "Diminuir velocidade",
"Slower (Left/Down)": "Mais lento (Esquerda/Baixo)",
"Current speed": "Velocidade atual",
"Increase speed": "Aumentar velocidade",
"Faster (Right/Up)": "Mais rápido (Direita/Cima)",
"Start RSVP Reading": "Iniciar leitura RSVP",
@@ -1077,5 +1075,11 @@
"Translating...": "Traduzindo...",
"Show Battery Percentage": "Mostrar porcentagem da bateria",
"Hide Scrollbar": "Ocultar barra de rolagem",
"Skip to last reading position": "Ir para a última posição de leitura"
"Skip to last reading position": "Ir para a última posição de leitura",
"Show context": "Mostrar contexto",
"Hide context": "Ocultar contexto",
"Decrease font size": "Diminuir tamanho da fonte",
"Increase font size": "Aumentar tamanho da fonte",
"Focus": "Foco",
"Theme color": "Cor do tema"
}
@@ -1014,10 +1014,8 @@
"Play (Space)": "Воспроизведение (Пробел)",
"Skip forward 15 words": "Вперед на 15 слов",
"Forward 15 words (Shift+Right)": "Вперед на 15 слов (Shift+Вправо)",
"Pause:": "Пауза:",
"Decrease speed": "Уменьшить скорость",
"Slower (Left/Down)": "Медленнее (Влево/Вниз)",
"Current speed": "Текущая скорость",
"Increase speed": "Увеличить скорость",
"Faster (Right/Up)": "Быстрее (Вправо/Вверх)",
"Start RSVP Reading": "Начать чтение RSVP",
@@ -1089,5 +1087,11 @@
"Translating...": "Перевод...",
"Show Battery Percentage": "Показывать процент заряда батареи",
"Hide Scrollbar": "Скрыть полосу прокрутки",
"Skip to last reading position": "Перейти к последней позиции чтения"
"Skip to last reading position": "Перейти к последней позиции чтения",
"Show context": "Показать контекст",
"Hide context": "Скрыть контекст",
"Decrease font size": "Уменьшить размер шрифта",
"Increase font size": "Увеличить размер шрифта",
"Focus": "Фокус",
"Theme color": "Цвет темы"
}
@@ -990,10 +990,8 @@
"Play (Space)": "ධාවනය (Space)",
"Skip forward 15 words": "වචන 15ක් ඉදිරියට",
"Forward 15 words (Shift+Right)": "වචන 15ක් ඉදිරියට (Shift+Right)",
"Pause:": "විරාමය:",
"Decrease speed": "වේගය අඩු කරන්න",
"Slower (Left/Down)": "මන්දගාමී (Left/Down)",
"Current speed": "වත්මන් වේගය",
"Increase speed": "වේගය වැඩි කරන්න",
"Faster (Right/Up)": "වේගවත් (Right/Up)",
"Start RSVP Reading": "RSVP කියවීම ආරම්භ කරන්න",
@@ -1065,5 +1063,11 @@
"Translating...": "පරිවර්තනය වෙමින් පවතී...",
"Show Battery Percentage": "බැටරි ප්‍රතිශතය පෙන්වන්න",
"Hide Scrollbar": "අනුචලන තීරුව සඟවන්න",
"Skip to last reading position": "අවසන් කියවීමේ ස්ථානයට යන්න"
"Skip to last reading position": "අවසන් කියවීමේ ස්ථානයට යන්න",
"Show context": "සන්දර්භය පෙන්වන්න",
"Hide context": "සන්දර්භය සඟවන්න",
"Decrease font size": "අකුරු ප්‍රමාණය අඩු කරන්න",
"Increase font size": "අකුරු ප්‍රමාණය වැඩි කරන්න",
"Focus": "අවධානය",
"Theme color": "තේමා වර්ණය"
}
@@ -532,10 +532,8 @@
"Play (Space)": "Predvajaj (Preslednica)",
"Skip forward 15 words": "Skoči naprej za 15 besed",
"Forward 15 words (Shift+Right)": "Naprej za 15 besed (Shift+Desno)",
"Pause:": "Premor:",
"Decrease speed": "Zmanjšaj hitrost",
"Slower (Left/Down)": "Počasneje (Levo/Dol)",
"Current speed": "Trenutna hitrost",
"Increase speed": "Povečaj hitrost",
"Faster (Right/Up)": "Hitreje (Desno/Gor)",
"Start RSVP Reading": "Začni RSVP branje",
@@ -1089,5 +1087,11 @@
"Show Current Battery Status": "Prikaži trenutno stanje baterije",
"Show Battery Percentage": "Prikaži odstotek baterije",
"Hide Scrollbar": "Skrij drsni trak",
"Skip to last reading position": "Skoči na zadnji položaj branja"
"Skip to last reading position": "Skoči na zadnji položaj branja",
"Show context": "Prikaži kontekst",
"Hide context": "Skrij kontekst",
"Decrease font size": "Pomanjšaj pisavo",
"Increase font size": "Povečaj pisavo",
"Focus": "Fokus",
"Theme color": "Barva teme"
}
@@ -990,10 +990,8 @@
"Play (Space)": "Spela (Mellanslag)",
"Skip forward 15 words": "Hoppa framåt 15 ord",
"Forward 15 words (Shift+Right)": "Framåt 15 ord (Shift+Höger)",
"Pause:": "Paus:",
"Decrease speed": "Sänk hastigheten",
"Slower (Left/Down)": "Långsammare (Vänster/Nedåt)",
"Current speed": "Nuvarande hastighet",
"Increase speed": "Höj hastigheten",
"Faster (Right/Up)": "Snabbare (Höger/Uppåt)",
"Start RSVP Reading": "Starta RSVP-läsning",
@@ -1065,5 +1063,11 @@
"Translating...": "Översätter...",
"Show Battery Percentage": "Visa batteriprocent",
"Hide Scrollbar": "Dölj rullningslist",
"Skip to last reading position": "Hoppa till senaste läsposition"
"Skip to last reading position": "Hoppa till senaste läsposition",
"Show context": "Visa kontext",
"Hide context": "Dölj kontext",
"Decrease font size": "Minska teckenstorlek",
"Increase font size": "Öka teckenstorlek",
"Focus": "Fokus",
"Theme color": "Temafärg"
}
@@ -990,10 +990,8 @@
"Play (Space)": "இயக்கு (Space)",
"Skip forward 15 words": "15 வார்த்தைகள் முன்னால் செல்",
"Forward 15 words (Shift+Right)": "15 வார்த்தைகள் முன்னால் செல் (Shift+Right)",
"Pause:": "நிறுத்து:",
"Decrease speed": "வேகத்தைக் குறை",
"Slower (Left/Down)": "மெதுவாக (Left/Down)",
"Current speed": "தற்போதைய வேகம்",
"Increase speed": "வேகத்தை அதிகரி",
"Faster (Right/Up)": "வேகமாக (Right/Up)",
"Start RSVP Reading": "RSVP வாசிப்பைத் தொடங்கு",
@@ -1065,5 +1063,11 @@
"Translating...": "மொழிபெயர்க்கிறது...",
"Show Battery Percentage": "பேட்டரி சதவீதத்தைக் காண்பி",
"Hide Scrollbar": "உருள்பட்டையை மறை",
"Skip to last reading position": "கடைசி வாசிப்பு நிலைக்குச் செல்"
"Skip to last reading position": "கடைசி வாசிப்பு நிலைக்குச் செல்",
"Show context": "சூழலைக் காட்டு",
"Hide context": "சூழலை மறை",
"Decrease font size": "எழுத்துரு அளவைக் குறை",
"Increase font size": "எழுத்துரு அளவை அதிகரி",
"Focus": "குவியம்",
"Theme color": "தீம் நிறம்"
}
@@ -978,10 +978,8 @@
"Play (Space)": "เล่น (Space)",
"Skip forward 15 words": "ไปข้างหน้า 15 คำ",
"Forward 15 words (Shift+Right)": "ไปข้างหน้า 15 คำ (Shift+ขวา)",
"Pause:": "หยุดชั่วคราว:",
"Decrease speed": "ลดความเร็ว",
"Slower (Left/Down)": "ช้าลง (ซ้าย/ลง)",
"Current speed": "ความเร็วปัจจุบัน",
"Increase speed": "เพิ่มความเร็ว",
"Faster (Right/Up)": "เร็วขึ้น (ขวา/ขึ้น)",
"Start RSVP Reading": "เริ่มการอ่านแบบ RSVP",
@@ -1053,5 +1051,11 @@
"Translating...": "กำลังแปล...",
"Show Battery Percentage": "แสดงเปอร์เซ็นต์แบตเตอรี่",
"Hide Scrollbar": "ซ่อนแถบเลื่อน",
"Skip to last reading position": "ข้ามไปตำแหน่งอ่านล่าสุด"
"Skip to last reading position": "ข้ามไปตำแหน่งอ่านล่าสุด",
"Show context": "แสดงบริบท",
"Hide context": "ซ่อนบริบท",
"Decrease font size": "ลดขนาดตัวอักษร",
"Increase font size": "เพิ่มขนาดตัวอักษร",
"Focus": "โฟกัส",
"Theme color": "สีธีม"
}
@@ -990,10 +990,8 @@
"Play (Space)": "Oynat (Boşluk)",
"Skip forward 15 words": "15 kelime ileri atla",
"Forward 15 words (Shift+Right)": "15 kelime ileri (Shift+Sağ)",
"Pause:": "Duraklat:",
"Decrease speed": "Hızı azalt",
"Slower (Left/Down)": "Daha yavaş (Sol/Aşağı)",
"Current speed": "Mevcut hız",
"Increase speed": "Hızı artır",
"Faster (Right/Up)": "Daha hızlı (Sağ/Yukarı)",
"Start RSVP Reading": "RSVP Okumasını Başlat",
@@ -1065,5 +1063,11 @@
"Translating...": "Çevriliyor...",
"Show Battery Percentage": "Pil Yüzdesini Göster",
"Hide Scrollbar": "Kaydırma çubuğunu gizle",
"Skip to last reading position": "Son okuma konumuna atla"
"Skip to last reading position": "Son okuma konumuna atla",
"Show context": "Bağlamı göster",
"Hide context": "Bağlamı gizle",
"Decrease font size": "Yazı tipi boyutunu küçült",
"Increase font size": "Yazı tipi boyutunu büyüt",
"Focus": "Odak",
"Theme color": "Tema rengi"
}
@@ -1014,10 +1014,8 @@
"Play (Space)": "Відтворити (Пробіл)",
"Skip forward 15 words": "Вперед на 15 слів",
"Forward 15 words (Shift+Right)": "Вперед на 15 слів (Shift+Стрілка вправо)",
"Pause:": "Пауза:",
"Decrease speed": "Зменшити швидкість",
"Slower (Left/Down)": "Повільніше (стрілки Вліво/Вниз)",
"Current speed": "Поточна швидкість",
"Increase speed": "Збільшити швидкість",
"Faster (Right/Up)": "Швидше (стрілки Вправо/Вгору)",
"Start RSVP Reading": "Почати читання RSVP",
@@ -1089,5 +1087,11 @@
"Translating...": "Переклад...",
"Show Battery Percentage": "Показувати відсоток заряду батареї",
"Hide Scrollbar": "Сховати смугу прокрутки",
"Skip to last reading position": "Перейти до останньої позиції читання"
"Skip to last reading position": "Перейти до останньої позиції читання",
"Show context": "Показати контекст",
"Hide context": "Сховати контекст",
"Decrease font size": "Зменшити розмір шрифту",
"Increase font size": "Збільшити розмір шрифту",
"Focus": "Фокус",
"Theme color": "Колір теми"
}
@@ -978,10 +978,8 @@
"Play (Space)": "Phát (Khoảng trắng)",
"Skip forward 15 words": "Tiến tới 15 từ",
"Forward 15 words (Shift+Right)": "Tiến tới 15 từ (Shift+Phải)",
"Pause:": "Tạm dừng:",
"Decrease speed": "Giảm tốc độ",
"Slower (Left/Down)": "Chậm hơn (Trái/Xuống)",
"Current speed": "Tốc độ hiện tại",
"Increase speed": "Tăng tốc độ",
"Faster (Right/Up)": "Nhanh hơn (Phải/Lên)",
"Start RSVP Reading": "Bắt đầu đọc RSVP",
@@ -1053,5 +1051,11 @@
"Translating...": "Đang dịch...",
"Show Battery Percentage": "Hiển thị phần trăm pin",
"Hide Scrollbar": "Ẩn thanh cuộn",
"Skip to last reading position": "Chuyển đến vị trí đọc cuối"
"Skip to last reading position": "Chuyển đến vị trí đọc cuối",
"Show context": "Hiển thị ngữ cảnh",
"Hide context": "Ẩn ngữ cảnh",
"Decrease font size": "Giảm cỡ chữ",
"Increase font size": "Tăng cỡ chữ",
"Focus": "Tiêu điểm",
"Theme color": "Màu chủ đề"
}
@@ -979,10 +979,8 @@
"Play (Space)": "播放 (空格)",
"Skip forward 15 words": "前进 15 词",
"Forward 15 words (Shift+Right)": "前进 15 词 (Shift+Right)",
"Pause:": "暂停:",
"Decrease speed": "减速",
"Slower (Left/Down)": "较慢 (左/下)",
"Current speed": "当前速度",
"Increase speed": "加速",
"Faster (Right/Up)": "较快 (右/上)",
"Start RSVP Reading": "开始 RSVP 阅读",
@@ -1053,5 +1051,11 @@
"Annotation page number": "批注页码",
"Translating...": "翻译中...",
"Show Battery Percentage": "显示电量百分比",
"Skip to last reading position": "跳转到上次阅读位置"
"Skip to last reading position": "跳转到上次阅读位置",
"Show context": "显示上下文",
"Hide context": "隐藏上下文",
"Decrease font size": "缩小字号",
"Increase font size": "放大字号",
"Focus": "焦点",
"Theme color": "主题颜色"
}
@@ -978,10 +978,8 @@
"Play (Space)": "播放 (空格)",
"Skip forward 15 words": "前進 15 詞",
"Forward 15 words (Shift+Right)": "前進 15 詞 (Shift+Right)",
"Pause:": "暫停:",
"Decrease speed": "減速",
"Slower (Left/Down)": "較慢 (左/下)",
"Current speed": "當前速度",
"Increase speed": "加速",
"Faster (Right/Up)": "較快 (右/上)",
"Start RSVP Reading": "開始 RSVP 閱讀",
@@ -1053,5 +1051,11 @@
"Translating...": "翻譯中...",
"Show Battery Percentage": "顯示電池百分比",
"Hide Scrollbar": "隱藏滾動條",
"Skip to last reading position": "跳至上次閱讀位置"
"Skip to last reading position": "跳至上次閱讀位置",
"Show context": "顯示上下文",
"Hide context": "隱藏上下文",
"Decrease font size": "縮小字型",
"Increase font size": "放大字型",
"Focus": "焦點",
"Theme color": "主題顏色"
}
+1 -1
View File
@@ -34,7 +34,7 @@ DEV_PID=$!
echo "Waiting for dev server on port $DEV_PORT..."
elapsed=0
while ! curl -sf "http://127.0.0.1:${DEV_PORT}" >/dev/null 2>&1; do
while ! curl -sf "http://localhost:${DEV_PORT}" >/dev/null 2>&1; do
if ! kill -0 "$DEV_PID" 2>/dev/null; then
echo "ERROR: Dev server exited unexpectedly."
exit 1
@@ -14,6 +14,8 @@ import {
IoPlaySkipForward,
IoRemove,
IoAdd,
IoChevronDown,
IoSettingsSharp,
} from 'react-icons/io5';
import { useTranslation } from '@/hooks/useTranslation';
import { Overlay } from '@/components/Overlay';
@@ -24,6 +26,14 @@ interface FlatChapter {
level: number;
}
// Display settings
const FONT_SIZE_OPTIONS = [1.25, 1.5, 1.875, 2.25, 3, 3.75, 4.25, 5, 6, 8];
const DEFAULT_FONT_SIZE_INDEX = 4;
const ORP_COLOR_OPTIONS = ['', '#EF4444', '#3B82F6', '#22C55E', '#F97316', '#A855F7'];
const STORAGE_KEY_FONT_SIZE = 'readest_rsvp_fontsize';
const STORAGE_KEY_ORP_COLOR = 'readest_rsvp_orp_color';
const STORAGE_KEY_CONTEXT = 'readest_rsvp_context';
interface RSVPOverlayProps {
gridInsets: Insets;
controller: RSVPController;
@@ -49,6 +59,49 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
const [currentWord, setCurrentWord] = useState<RsvpWord | null>(controller.currentWord);
const [countdown, setCountdown] = useState<number | null>(controller.currentCountdown);
const [showChapterDropdown, setShowChapterDropdown] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [contextCollapsed, setContextCollapsed] = useState(() => {
try {
return localStorage.getItem(STORAGE_KEY_CONTEXT) === '1';
} catch {
return false;
}
});
const [fontSizeIndex, setFontSizeIndex] = useState(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY_FONT_SIZE);
if (saved !== null) {
const idx = parseInt(saved, 10);
if (idx >= 0 && idx < FONT_SIZE_OPTIONS.length) return idx;
}
} catch {
/* ignore */
}
return DEFAULT_FONT_SIZE_INDEX;
});
const [orpColorIndex, setOrpColorIndex] = useState(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY_ORP_COLOR);
if (saved !== null) {
const idx = parseInt(saved, 10);
if (idx >= 0 && idx < ORP_COLOR_OPTIONS.length) return idx;
}
} catch {
/* ignore */
}
return 0;
});
// Context window: only rebuild the rendered word list when the current word
// falls outside the window or nears its edge, keeping the DOM stable.
const [contextWindow, setContextWindow] = useState(() => {
if (!state || state.words.length === 0) return { start: 0, end: 0 };
return {
start: Math.max(0, state.currentIndex - 50),
end: Math.min(state.words.length, state.currentIndex + 151),
};
});
const contextWordRef = useRef<HTMLSpanElement>(null);
const contextPanelRef = useRef<HTMLDivElement>(null);
const touchStartX = useRef(0);
const touchStartY = useRef(0);
const touchStartTime = useRef(0);
@@ -76,6 +129,27 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
const newState = (e as CustomEvent<RsvpState>).detail;
setState(newState);
setCurrentWord(controller.currentWord);
// Update context window only when current word falls outside or nears edge
const idx = newState.currentIndex;
const total = newState.words.length;
setContextWindow((prev) => {
if (total === 0) return { start: 0, end: 0 };
// Outside window — reset
if (idx < prev.start || idx >= prev.end) {
return {
start: Math.max(0, idx - 50),
end: Math.min(total, idx + 151),
};
}
// Near end of window — extend forward
const windowSize = prev.end - prev.start;
if (idx - prev.start > windowSize * 0.8) {
return { start: prev.start, end: Math.min(total, prev.end + 100) };
}
// No change — return same reference to avoid re-render
return prev;
});
};
const handleCountdownChange = (e: Event) => {
@@ -174,24 +248,60 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
}
}, [state]);
// Context text helpers - show 100 words before and after
const getContextBefore = useCallback((): string => {
if (!state || state.words.length === 0) return '';
const startIndex = Math.max(0, state.currentIndex - 100);
return state.words
.slice(startIndex, state.currentIndex)
.map((w) => w.text)
.join(' ');
}, [state]);
// Stable word list that only changes when contextWindow changes
const contextWords = useMemo(
() => state.words.slice(contextWindow.start, contextWindow.end),
[state.words, contextWindow],
);
const getContextAfter = useCallback((): string => {
if (!state || state.words.length === 0) return '';
const endIndex = Math.min(state.words.length, state.currentIndex + 101);
return state.words
.slice(state.currentIndex + 1, endIndex)
.map((w) => w.text)
.join(' ');
}, [state]);
// Auto-scroll: keep highlighted word away from top/bottom edges
useEffect(() => {
const panel = contextPanelRef.current;
const word = contextWordRef.current;
if (contextCollapsed || !panel || !word) return;
const panelRect = panel.getBoundingClientRect();
const wordRect = word.getBoundingClientRect();
const margin = panelRect.height * 0.15;
const topLine = panelRect.top + margin;
if (wordRect.top < topLine) {
panel.scrollTop -= topLine - wordRect.top;
} else if (wordRect.bottom > panelRect.bottom - margin) {
panel.scrollTop += wordRect.top - topLine;
}
}, [state.currentIndex, contextCollapsed]);
const toggleContext = useCallback(() => {
setContextCollapsed((prev) => {
const next = !prev;
try {
localStorage.setItem(STORAGE_KEY_CONTEXT, next ? '1' : '0');
} catch {
/* ignore */
}
return next;
});
}, []);
const updateFontSize = useCallback((idx: number) => {
const clamped = Math.max(0, Math.min(FONT_SIZE_OPTIONS.length - 1, idx));
setFontSizeIndex(clamped);
try {
localStorage.setItem(STORAGE_KEY_FONT_SIZE, String(clamped));
} catch {
/* ignore */
}
}, []);
const updateOrpColor = useCallback((idx: number) => {
setOrpColorIndex(idx);
try {
localStorage.setItem(STORAGE_KEY_ORP_COLOR, String(idx));
} catch {
/* ignore */
}
}, []);
// Chapter helpers
const getCurrentChapterLabel = useCallback((): string => {
@@ -290,6 +400,9 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
const bgColor = themeCode.bg;
const fgColor = themeCode.fg;
const accentColor = themeCode.primary;
const effectiveOrpColor = ORP_COLOR_OPTIONS[orpColorIndex] || accentColor;
const currentFontSize =
FONT_SIZE_OPTIONS[fontSizeIndex] ?? FONT_SIZE_OPTIONS[DEFAULT_FONT_SIZE_INDEX]!;
return (
<div
@@ -373,35 +486,60 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
</div>
</div>
{/* Context panel (shown when paused) */}
{!state.playing && countdown === null && (
<div className='mx-3 max-h-[25vh] overflow-y-auto rounded-lg border border-gray-500/20 bg-gray-500/10 p-3 md:mx-4 md:max-h-[30vh] md:rounded-xl md:p-4'>
<div className='mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide opacity-60 md:mb-3'>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
className='md:h-4 md:w-4'
>
<path d='M4 6h16M4 12h16M4 18h10' />
</svg>
<span>{_('Context')}</span>
{/* Context panel (always visible, collapsible) */}
<div className='mx-3 overflow-hidden rounded-lg border border-gray-500/20 bg-gray-500/10 md:mx-4 md:rounded-xl'>
<button
className='flex w-full items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wide opacity-60 transition-opacity hover:opacity-80 md:px-4 md:py-3'
onClick={toggleContext}
aria-expanded={!contextCollapsed}
aria-label={contextCollapsed ? _('Show context') : _('Hide context')}
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
className='md:h-4 md:w-4'
>
<path d='M4 6h16M4 12h16M4 18h10' />
</svg>
<span className='flex-1 text-left'>{_('Context')}</span>
<IoChevronDown
className={clsx(
'h-3.5 w-3.5 transition-transform duration-200',
!contextCollapsed && 'rotate-180',
)}
/>
</button>
{!contextCollapsed && (
<div
ref={contextPanelRef}
className='max-h-[20vh] overflow-y-auto px-3 pb-3 md:px-4 md:pb-4'
>
<div className='text-left text-base leading-relaxed md:text-lg'>
{contextWords.map((w, i) => {
const wordIndex = contextWindow.start + i;
const isCurrent = wordIndex === state.currentIndex;
return (
<span
key={wordIndex}
ref={isCurrent ? contextWordRef : undefined}
className={isCurrent ? undefined : 'opacity-70'}
style={isCurrent ? { color: effectiveOrpColor } : undefined}
>
{w.text}{' '}
</span>
);
})}
</div>
</div>
<div className='text-left text-base leading-relaxed md:text-lg'>
<span className='opacity-70'>{getContextBefore()} </span>
<span className='font-semibold' style={{ color: accentColor }}>
{currentWord?.text || ''}
</span>
<span className='opacity-70'> {getContextAfter()}</span>
</div>
</div>
)}
)}
</div>
{/* Main content area */}
<div className='flex flex-1 flex-col items-center justify-center p-4 md:p-8'>
<div className='flex flex-1 flex-col items-center justify-center p-4 md:p-6'>
<div className='flex h-full w-full flex-col items-center justify-center'>
<div className='flex h-full w-full flex-col items-center'>
{/* Top guide line */}
@@ -422,16 +560,22 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
)}
{/* Word display */}
<div className='relative flex min-h-16 w-full items-center justify-center whitespace-nowrap px-2 py-4 font-mono text-2xl font-medium tracking-wide sm:min-h-20 sm:px-4 sm:py-6 sm:text-3xl md:text-4xl lg:text-5xl'>
<div
className='rsvp-word relative flex min-h-16 w-full items-center justify-center whitespace-nowrap px-2 py-2 font-mono font-medium leading-none tracking-wide sm:min-h-20 sm:px-4 sm:py-4'
style={{ fontSize: `${currentFontSize}rem` }}
>
{currentWord ? (
<>
<span className='absolute right-[calc(50%+0.3em)] text-right opacity-60'>
<span className='rsvp-word-before absolute right-[calc(50%+0.3em)] text-right opacity-60'>
{wordBefore}
</span>
<span className='relative z-10 font-bold' style={{ color: accentColor }}>
<span
className='rsvp-word-orp relative z-10 font-bold'
style={{ color: effectiveOrpColor }}
>
{orpChar}
</span>
<span className='absolute left-[calc(50%+0.3em)] text-left opacity-60'>
<span className='rsvp-word-after absolute left-[calc(50%+0.3em)] text-left opacity-60'>
{wordAfter}
</span>
</>
@@ -494,96 +638,140 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
</div>
</div>
{/* Controls */}
<div className='flex flex-col gap-3 md:flex-row md:items-center md:justify-between md:gap-4'>
{/* Playback controls - centered on mobile, middle on desktop */}
<div className='flex items-center justify-center gap-2 md:order-2 md:gap-4'>
<button
aria-label={_('Skip back 15 words')}
className='flex cursor-pointer items-center gap-1 rounded-full border-none bg-transparent px-2 py-1.5 transition-colors hover:bg-gray-500/20 active:scale-95 md:px-3 md:py-2'
onClick={() => controller.skipBackward(15)}
title={_('Back 15 words (Shift+Left)')}
>
<span className='text-xs font-semibold opacity-80'>15</span>
<IoPlaySkipBack className='h-5 w-5 md:h-6 md:w-6' />
</button>
{/* Playback controls */}
<div className='relative flex items-center justify-center gap-1 md:gap-2'>
<button
aria-label={_('Skip back 15 words')}
className='flex cursor-pointer items-center gap-0.5 rounded-full border-none bg-transparent px-2 py-1.5 transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => controller.skipBackward(15)}
title={_('Back 15 words (Shift+Left)')}
>
<span className='text-xs font-semibold opacity-80'>15</span>
<IoPlaySkipBack className='h-5 w-5 md:h-6 md:w-6' />
</button>
<button
aria-label={state.playing ? _('Pause') : _('Play')}
className={clsx(
'flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-gray-500/15 transition-colors hover:bg-gray-500/25 active:scale-95 md:h-16 md:w-16',
state.playing ? '' : 'ps-1',
)}
onClick={() => controller.togglePlayPause()}
title={state.playing ? _('Pause (Space)') : _('Play (Space)')}
>
{state.playing ? (
<IoPause className='h-7 w-7 md:h-8 md:w-8' />
) : (
<IoPlay className='h-7 w-7 md:h-8 md:w-8' />
)}
</button>
<button
aria-label={_('Decrease speed')}
className='flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => controller.decreaseSpeed()}
title={_('Slower (Left/Down)')}
>
<IoRemove className='h-4 w-4 md:h-5 md:w-5' />
</button>
<button
aria-label={_('Skip forward 15 words')}
className='flex cursor-pointer items-center gap-1 rounded-full border-none bg-transparent px-2 py-1.5 transition-colors hover:bg-gray-500/20 active:scale-95 md:px-3 md:py-2'
onClick={() => controller.skipForward(15)}
title={_('Forward 15 words (Shift+Right)')}
>
<IoPlaySkipForward className='h-5 w-5 md:h-6 md:w-6' />
<span className='text-xs font-semibold opacity-80'>15</span>
</button>
</div>
<button
aria-label={state.playing ? _('Pause') : _('Play')}
className={clsx(
'flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-gray-500/15 transition-colors hover:bg-gray-500/25 active:scale-95 md:h-16 md:w-16',
state.playing ? '' : 'ps-1',
)}
onClick={() => controller.togglePlayPause()}
title={state.playing ? _('Pause (Space)') : _('Play (Space)')}
>
{state.playing ? (
<IoPause className='h-7 w-7 md:h-8 md:w-8' />
) : (
<IoPlay className='h-7 w-7 md:h-8 md:w-8' />
)}
</button>
{/* Secondary controls row on mobile, split on desktop */}
<div className='flex items-center justify-between gap-4 md:contents'>
{/* Punctuation pause - left on desktop */}
<div className='flex items-center md:order-1 md:min-w-[140px] md:flex-1'>
<label className='flex cursor-pointer items-center gap-1.5 text-xs font-medium opacity-80 md:gap-2'>
<span className='hidden sm:inline'>{_('Pause:')}</span>
<span className='sm:hidden'>{_('Pause:')}</span>
<select
className='cursor-pointer rounded border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30 md:px-2'
style={{ color: 'inherit' }}
value={state.punctuationPauseMs}
onChange={(e) => controller.setPunctuationPause(parseInt(e.target.value, 10))}
>
{controller.getPunctuationPauseOptions().map((option) => (
<option key={option} value={option}>
{option}ms
</option>
))}
</select>
</label>
</div>
<button
aria-label={_('Increase speed')}
className='flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => controller.increaseSpeed()}
title={_('Faster (Right/Up)')}
>
<IoAdd className='h-4 w-4 md:h-5 md:w-5' />
</button>
{/* Speed controls - right on desktop */}
<div className='flex items-center justify-end gap-1.5 md:order-3 md:min-w-[140px] md:flex-1 md:gap-2'>
<button
aria-label={_('Skip forward 15 words')}
className='flex cursor-pointer items-center gap-0.5 rounded-full border-none bg-transparent px-2 py-1.5 transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => controller.skipForward(15)}
title={_('Forward 15 words (Shift+Right)')}
>
<IoPlaySkipForward className='h-5 w-5 md:h-6 md:w-6' />
<span className='text-xs font-semibold opacity-80'>15</span>
</button>
<button
aria-label={_('Settings')}
className={clsx(
'absolute right-0 flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95',
showSettings && 'bg-gray-500/15',
)}
onClick={() => setShowSettings((prev) => !prev)}
title={_('Settings')}
>
<IoSettingsSharp className='h-4 w-4 md:h-5 md:w-5' />
</button>
</div>
{/* Settings row (collapsible) */}
{showSettings && (
<div className='mt-3 flex flex-wrap items-center justify-evenly gap-x-8 gap-y-4 text-xs md:justify-center'>
{/* Punctuation pause */}
<label className='flex cursor-pointer items-center gap-1.5 font-medium opacity-80'>
<span className='mr-0.5 font-medium opacity-50'>{_('Pause')}</span>
<select
className='cursor-pointer rounded border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30'
style={{ color: 'inherit' }}
value={state.punctuationPauseMs}
onChange={(e) => controller.setPunctuationPause(parseInt(e.target.value, 10))}
>
{controller.getPunctuationPauseOptions().map((option) => (
<option key={option} value={option}>
{option}ms
</option>
))}
</select>
</label>
{/* Font size */}
<div className='flex items-center gap-0.5'>
<span className='mr-0.5 font-medium opacity-50'>{_('Font')}</span>
<button
aria-label={_('Decrease speed')}
className='flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95 md:h-10 md:w-10'
onClick={() => controller.decreaseSpeed()}
title={_('Slower (Left/Down)')}
aria-label={_('Decrease font size')}
className='flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => updateFontSize(fontSizeIndex - 1)}
disabled={fontSizeIndex <= 0}
>
<IoRemove className='h-4 w-4 md:h-5 md:w-5' />
<IoRemove className='h-3 w-3' />
</button>
<span
aria-label={_('Current speed')}
className='min-w-10 text-center text-sm font-medium md:min-w-12'
>
{state.wpm}
<span className='min-w-4 text-center font-medium tabular-nums'>
{fontSizeIndex + 1}
</span>
<button
aria-label={_('Increase speed')}
className='flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95 md:h-10 md:w-10'
onClick={() => controller.increaseSpeed()}
title={_('Faster (Right/Up)')}
aria-label={_('Increase font size')}
className='flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border-none bg-transparent transition-colors hover:bg-gray-500/20 active:scale-95'
onClick={() => updateFontSize(fontSizeIndex + 1)}
disabled={fontSizeIndex >= FONT_SIZE_OPTIONS.length - 1}
>
<IoAdd className='h-4 w-4 md:h-5 md:w-5' />
<IoAdd className='h-3 w-3' />
</button>
</div>
{/* ORP color */}
<div className='flex items-center gap-1.5'>
<span className='mr-0.5 font-medium opacity-50'>{_('Focus')}</span>
{ORP_COLOR_OPTIONS.map((color, idx) => (
<button
key={idx}
onClick={() => updateOrpColor(idx)}
className={clsx(
'h-6 min-h-6 w-6 min-w-6 rounded-full border-2 transition-transform',
orpColorIndex === idx
? 'scale-110 border-current'
: 'border-transparent hover:scale-105',
)}
style={{ backgroundColor: color || accentColor }}
aria-label={idx === 0 ? _('Theme color') : `Color ${idx}`}
title={idx === 0 ? _('Theme color') : undefined}
/>
))}
</div>
</div>
</div>
)}
</div>
</div>
);
+2 -2
View File
@@ -5,7 +5,7 @@ export type Insets = {
right: number;
};
export interface LocaleWithTextInfo extends Intl.Locale {
export type LocaleWithTextInfo = Intl.Locale & {
getTextInfo?: () => { direction: string };
textInfo?: { direction: string };
}
};
+2 -2
View File
@@ -72,9 +72,9 @@ export const handleA11yNavigation = (
const skipLink = document.createElement('div');
skipLink.id = skipLinkId;
skipLink.setAttribute('cfi-inert', '');
skipLink.setAttribute('aria-hidden', 'false');
skipLink.setAttribute('tabindex', '0');
skipLink.textContent = options?.skipToLastPosLabel ?? '';
skipLink.setAttribute('aria-hidden', 'false');
skipLink.setAttribute('aria-label', options?.skipToLastPosLabel ?? '');
Object.assign(skipLink.style, {
position: 'absolute',
left: '0px',
+73
View File
@@ -438,6 +438,9 @@ importers:
'@typescript-eslint/parser':
specifier: ^8.48.0
version: 8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
'@typescript/native-preview':
specifier: 7.0.0-dev.20260312.1
version: 7.0.0-dev.20260312.1
'@vitejs/plugin-react':
specifier: ^5.1.1
version: 5.1.2(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
@@ -4314,6 +4317,45 @@ packages:
resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-AhPdPuVe4osxWoeImS21jVhc0VJ2QnzLUZtEFMakY0Rf70C0b6il/m7hwRf9wkr9xXZLVOVJ1kYrpvQRuHFE0Q==}
cpu: [arm64]
os: [darwin]
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-9I0P1/c/mQ6UVcQq7SYY/FJD23IN5T2y4GbSFOKQvzNVASV0tMnX4YV8YNf6b5jcwCzrVcrGNKKgWCj8xEFf8Q==}
cpu: [x64]
os: [darwin]
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-xwoMywagcvx9F2ocM+ybeg7eH9PHDpx1FBGOrloL1/xkGC4BCrn/RcaAe0AhzXzoJfHHmg7Sz9VzYmTR4N1Kqw==}
cpu: [arm64]
os: [linux]
'@typescript/native-preview-linux-arm@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-/nAOhSLTxMJfHY+2cKdUxi2wYadf3g1GtC3VzgPfZMNxA28dJ8x75T26aSLaFYluh7cCSAwuGesCImijQDS2Lw==}
cpu: [arm]
os: [linux]
'@typescript/native-preview-linux-x64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-vZs0LLpZw50Ac0TCmF9ND7KphJBhOfp9fxLhC+hFWaUU1iCQRjv1MtvroitF5OJKb21qFPJxkU+kfhlCRxLfqg==}
cpu: [x64]
os: [linux]
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-4LY/gd9cj1xDY2nEthB7WDW4j/fIYJ9wp9H71nOLd0wNNtkfqRXWSkQEeb+RByhV+dIb/n6kWbQQMeNfk7q4VQ==}
cpu: [arm64]
os: [win32]
'@typescript/native-preview-win32-x64@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-EP2JPo9s9EPUwXSX83qTImlDHhgkLeBbJ2MMdj+XrfBltHAvHKktzeSS73UhP77s/TnTkJR6BTWHENKKvLRbGQ==}
cpu: [x64]
os: [win32]
'@typescript/native-preview@7.0.0-dev.20260312.1':
resolution: {integrity: sha512-FwhlXG/yG0d7b2UmooBYyszLMpICRYdYGE6v65ZlMnH7cWKQyyFpMFgH9suRf3Np4QCbN+7qisj+F23kQOidVw==}
hasBin: true
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -14494,6 +14536,37 @@ snapshots:
'@typescript-eslint/types': 8.53.1
eslint-visitor-keys: 4.2.1
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-linux-arm@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-linux-x64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview-win32-x64@7.0.0-dev.20260312.1':
optional: true
'@typescript/native-preview@7.0.0-dev.20260312.1':
optionalDependencies:
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260312.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260312.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260312.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260312.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260312.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260312.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260312.1
'@ungap/structured-clone@1.3.0': {}
'@unpic/core@1.0.3':