forked from akai/readest
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f815844577 | |||
| 7fe4d38c51 | |||
| 43edcb4e9e | |||
| e259b29afa | |||
| cd5f57a004 | |||
| 8c0cdb0e22 | |||
| c0df9f1cb6 | |||
| cb1ddaeedb | |||
| 4447dd7b9a | |||
| dcb7602837 | |||
| 60e48742fe | |||
| dd43569778 | |||
| 8433dd6950 | |||
| 379802f580 | |||
| 2dcb5d7c1e | |||
| 98870d9f3e | |||
| 31aeb2289c | |||
| 0f66ecbba1 | |||
| f0d3c9dd4e | |||
| 92aae340d9 |
@@ -177,6 +177,11 @@ jobs:
|
||||
pnpm tauri icon ../../data/icons/readest-book.png
|
||||
git checkout .
|
||||
|
||||
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
|
||||
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST" || \
|
||||
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
|
||||
|
||||
pushd src-tauri/gen/android
|
||||
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
||||
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Upload Release Assets to R2
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag name (e.g., v1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
upload-to-r2:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RELEASE_R2_BUCKET: readest-releases
|
||||
RELEASE_R2_ACCOUNT_ID: ${{ secrets.RELEASE_R2_ACCOUNT_ID }}
|
||||
RELEASE_R2_ACCESS_KEY_ID: ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
|
||||
RELEASE_R2_SECRET_ACCESS_KEY: ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Download release assets
|
||||
run: |
|
||||
gh release download "${{ inputs.tag }}" --repo readest/readest --dir ./release-assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install rclone
|
||||
run: curl https://rclone.org/install.sh | sudo bash
|
||||
|
||||
- name: Configure rclone
|
||||
run: |
|
||||
mkdir -p ~/.config/rclone
|
||||
cat > ~/.config/rclone/rclone.conf <<EOF
|
||||
[r2]
|
||||
type = s3
|
||||
provider = Cloudflare
|
||||
access_key_id = $RELEASE_R2_ACCESS_KEY_ID
|
||||
secret_access_key = $RELEASE_R2_SECRET_ACCESS_KEY
|
||||
endpoint = https://${RELEASE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||
EOF
|
||||
|
||||
- name: Modify latest.json download URLs
|
||||
run: |
|
||||
GITHUB_BASE_URL="https://github.com/readest/readest/releases/download"
|
||||
READEST_BASE_URL="https://download.readest.com/releases"
|
||||
sed -i "s#${GITHUB_BASE_URL}#${READEST_BASE_URL}#g" ./release-assets/latest.json
|
||||
|
||||
- name: Upload to R2
|
||||
run: |
|
||||
mkdir releases
|
||||
mv ./release-assets/latest.json releases
|
||||
mv ./release-assets/release-notes.json releases
|
||||
rclone copy ./release-assets r2:${RELEASE_R2_BUCKET}/releases/${{ inputs.tag }}/
|
||||
rclone copy ./releases r2:${RELEASE_R2_BUCKET}/releases/
|
||||
@@ -87,7 +87,7 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.58",
|
||||
"version": "0.9.60",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -30,6 +30,7 @@
|
||||
"build-ios-appstore": "dotenv -e .env.ios-appstore.local -- tauri ios build --export-method app-store-connect",
|
||||
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh",
|
||||
"release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh",
|
||||
"release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh",
|
||||
"config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "مسح البحث",
|
||||
"Header & Footer": "الترويسة والتذييل",
|
||||
"Apply also in Scrolled Mode": "تطبيق أيضًا في وضع التمرير",
|
||||
"A new version of Readest is Available!": "يتوفر إصدار جديد من Readest!",
|
||||
"A new version of Readest is available!": "يتوفر إصدار جديد من Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار {{newVersion}} من ريديست متاح للتنزيل (الإصدار المثبت حاليًا {{currentVersion}}).",
|
||||
"Download and install now?": "هل ترغب في تنزيله وتثبيته الآن؟",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
|
||||
@@ -375,5 +375,6 @@
|
||||
"Right Margin (px)": "الهامش الأيمن (بكسل)",
|
||||
"Left Margin (px)": "الهامش الأيسر (بكسل)",
|
||||
"Column Gap (%)": "تباعد الأعمدة (%)",
|
||||
"Always Show Status Bar": "دائمًا إظهار شريط الحالة"
|
||||
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
|
||||
"Translation Not Available": "الترجمة غير متاحة"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Suche löschen",
|
||||
"Header & Footer": "Kopf- & Fußzeile",
|
||||
"Apply also in Scrolled Mode": "Auch im Scroll-Modus anwenden",
|
||||
"A new version of Readest is Available!": "Eine neue Version von Readest ist verfügbar!",
|
||||
"A new version of Readest is available!": "Eine neue Version von Readest ist verfügbar!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} ist verfügbar (installierte Version {{currentVersion}}).",
|
||||
"Download and install now?": "Jetzt herunterladen und installieren?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
|
||||
@@ -363,5 +363,6 @@
|
||||
"Right Margin (px)": "Rechter Rand (px)",
|
||||
"Left Margin (px)": "Linker Rand (px)",
|
||||
"Column Gap (%)": "Spaltenabstand (%)",
|
||||
"Always Show Status Bar": "Statusleiste immer anzeigen"
|
||||
"Always Show Status Bar": "Statusleiste immer anzeigen",
|
||||
"Translation Not Available": "Übersetzung nicht verfügbar"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Καθαρισμός αναζήτησης",
|
||||
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
|
||||
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
|
||||
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
|
||||
"A new version of Readest is available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
|
||||
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
|
||||
@@ -363,5 +363,6 @@
|
||||
"Right Margin (px)": "Δεξί περιθώριο (px)",
|
||||
"Left Margin (px)": "Αριστερό περιθώριο (px)",
|
||||
"Column Gap (%)": "Απόσταση στηλών (%)",
|
||||
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης"
|
||||
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
|
||||
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Limpiar búsqueda",
|
||||
"Header & Footer": "Encabezado y pie de página",
|
||||
"Apply also in Scrolled Mode": "Aplicar también en modo desplazamiento",
|
||||
"A new version of Readest is Available!": "¡Una nueva versión de Readest está disponible!",
|
||||
"A new version of Readest is available!": "¡Una nueva versión de Readest está disponible!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponible (versión instalada {{currentVersion}}).",
|
||||
"Download and install now?": "Descargar e instalar ahora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
|
||||
@@ -366,5 +366,6 @@
|
||||
"Right Margin (px)": "Margen derecho (px)",
|
||||
"Left Margin (px)": "Margen izquierdo (px)",
|
||||
"Column Gap (%)": "Espacio entre columnas (%)",
|
||||
"Always Show Status Bar": "Mostrar siempre la barra de estado"
|
||||
"Always Show Status Bar": "Mostrar siempre la barra de estado",
|
||||
"Translation Not Available": "Traducción no disponible"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Effacer la recherche",
|
||||
"Header & Footer": "En-tête et pied de page",
|
||||
"Apply also in Scrolled Mode": "Appliquer également en mode défilement",
|
||||
"A new version of Readest is Available!": "Avis de mise à jour",
|
||||
"A new version of Readest is available!": "Avis de mise à jour",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} est disponible (version installée {{currentVersion}}).",
|
||||
"Download and install now?": "Télécharger et installer maintenant ?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
|
||||
@@ -366,5 +366,6 @@
|
||||
"Right Margin (px)": "Marge droite (px)",
|
||||
"Left Margin (px)": "Marge gauche (px)",
|
||||
"Column Gap (%)": "Espacement des colonnes (%)",
|
||||
"Always Show Status Bar": "Afficher toujours la barre d'état"
|
||||
"Always Show Status Bar": "Afficher toujours la barre d'état",
|
||||
"Translation Not Available": "Traduction non disponible"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "खोज साफ करें",
|
||||
"Header & Footer": "हेडर और फ़ुटर",
|
||||
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
|
||||
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
|
||||
"A new version of Readest is available!": "Readest का एक नया संस्करण उपलब्ध है!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
|
||||
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
|
||||
@@ -363,5 +363,6 @@
|
||||
"Right Margin (px)": "दायां हाशिया (px)",
|
||||
"Left Margin (px)": "बायां हाशिया (px)",
|
||||
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
|
||||
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं"
|
||||
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
|
||||
"Translation Not Available": "अनुवाद उपलब्ध नहीं है"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Hapus Pencarian",
|
||||
"Header & Footer": "Header & Footer",
|
||||
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
|
||||
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
|
||||
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
|
||||
"Download and install now?": "Unduh dan instal sekarang?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "Margin Kanan (px)",
|
||||
"Left Margin (px)": "Margin Kiri (px)",
|
||||
"Column Gap (%)": "Jarak Antar Kolom (%)",
|
||||
"Always Show Status Bar": "Selalu Tampilkan Bilah Status"
|
||||
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
|
||||
"Translation Not Available": "Terjemahan Tidak Tersedia"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Cancella ricerca",
|
||||
"Header & Footer": "Intestazione e piè di pagina",
|
||||
"Apply also in Scrolled Mode": "Applica anche in modalità scorrimento",
|
||||
"A new version of Readest is Available!": "È disponibile una nuova versione di Readest!",
|
||||
"A new version of Readest is available!": "È disponibile una nuova versione di Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} è disponibile (versione installata {{currentVersion}}).",
|
||||
"Download and install now?": "Scarica e installa ora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
|
||||
@@ -366,5 +366,6 @@
|
||||
"Right Margin (px)": "Margine Destro (px)",
|
||||
"Left Margin (px)": "Margine Sinistro (px)",
|
||||
"Column Gap (%)": "Spazio tra Colonne (%)",
|
||||
"Always Show Status Bar": "Mostra sempre la barra di stato"
|
||||
"Always Show Status Bar": "Mostra sempre la barra di stato",
|
||||
"Translation Not Available": "Traduzione non disponibile"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "検索をクリア",
|
||||
"Header & Footer": "ヘッダーとフッター",
|
||||
"Apply also in Scrolled Mode": "スクロールモードでも適用",
|
||||
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"A new version of Readest is available!": "新しいバージョンのReadestが利用可能です!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
|
||||
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "右マージン (px)",
|
||||
"Left Margin (px)": "左マージン (px)",
|
||||
"Column Gap (%)": "列間の間隔 (%)",
|
||||
"Always Show Status Bar": "ステータスバーを常に表示"
|
||||
"Always Show Status Bar": "ステータスバーを常に表示",
|
||||
"Translation Not Available": "翻訳は利用できません"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "검색 지우기",
|
||||
"Header & Footer": "헤더 및 푸터",
|
||||
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
|
||||
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
|
||||
"A new version of Readest is available!": "Readest의 새 버전이 있습니다!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
|
||||
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "오른쪽 여백 (px)",
|
||||
"Left Margin (px)": "왼쪽 여백 (px)",
|
||||
"Column Gap (%)": "열 간격 (%)",
|
||||
"Always Show Status Bar": "상태 표시줄 항상 표시"
|
||||
"Always Show Status Bar": "상태 표시줄 항상 표시",
|
||||
"Translation Not Available": "번역을 사용할 수 없음"
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
"Description:": "Beschrijving:",
|
||||
"No description available": "Geen beschrijving beschikbaar",
|
||||
"Drop to Import Books": "Sleep om boeken te importeren",
|
||||
"A new version of Readest is Available!": "Een nieuwe versie van Readest is beschikbaar!",
|
||||
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
|
||||
"Download and install now?": "Nu downloaden en installeren?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
|
||||
@@ -363,5 +363,6 @@
|
||||
"Right Margin (px)": "Rechter marge (px)",
|
||||
"Left Margin (px)": "Linker marge (px)",
|
||||
"Column Gap (%)": "Kolomafstand (%)",
|
||||
"Always Show Status Bar": "Statusbalk altijd weergeven"
|
||||
"Always Show Status Bar": "Statusbalk altijd weergeven",
|
||||
"Translation Not Available": "Vertaling niet beschikbaar"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Wyczyść wyszukiwanie",
|
||||
"Header & Footer": "Nagłówek i stopka",
|
||||
"Apply also in Scrolled Mode": "Zastosuj również w trybie przewijania",
|
||||
"A new version of Readest is Available!": "Nowa wersja Readest jest dostępna!",
|
||||
"A new version of Readest is available!": "Nowa wersja Readest jest dostępna!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} jest dostępny (zainstalowana wersja {{currentVersion}}).",
|
||||
"Download and install now?": "Pobierz i zainstaluj teraz?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
|
||||
@@ -369,5 +369,6 @@
|
||||
"Right Margin (px)": "Prawy margines (px)",
|
||||
"Left Margin (px)": "Lewy margines (px)",
|
||||
"Column Gap (%)": "Odstęp między kolumnami (%)",
|
||||
"Always Show Status Bar": "Zawsze pokazuj pasek stanu"
|
||||
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
|
||||
"Translation Not Available": "Brak dostępnego tłumaczenia"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Limpar Pesquisa",
|
||||
"Header & Footer": "Cabeçalho e Rodapé",
|
||||
"Apply also in Scrolled Mode": "Aplicar também no Modo Rolado",
|
||||
"A new version of Readest is Available!": "Uma nova versão do Readest está disponível!",
|
||||
"A new version of Readest is available!": "Uma nova versão do Readest está disponível!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponível (versão instalada {{currentVersion}}).",
|
||||
"Download and install now?": "Baixar e instalar agora?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
|
||||
@@ -366,5 +366,6 @@
|
||||
"Right Margin (px)": "Margem direita (px)",
|
||||
"Left Margin (px)": "Margem esquerda (px)",
|
||||
"Column Gap (%)": "Espaço entre colunas (%)",
|
||||
"Always Show Status Bar": "Exibir sempre a barra de status"
|
||||
"Always Show Status Bar": "Exibir sempre a barra de status",
|
||||
"Translation Not Available": "Tradução não disponível"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Очистить поиск",
|
||||
"Header & Footer": "Заголовок и нижний колонтитул",
|
||||
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
|
||||
"A new version of Readest is Available!": "Доступна новая версия Readest!",
|
||||
"A new version of Readest is available!": "Доступна новая версия Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
|
||||
"Download and install now?": "Скачать и установить сейчас?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
|
||||
@@ -369,5 +369,6 @@
|
||||
"Right Margin (px)": "Правый отступ (пикс)",
|
||||
"Left Margin (px)": "Левый отступ (пикс)",
|
||||
"Column Gap (%)": "Промежуток между колонками (%)",
|
||||
"Always Show Status Bar": "Всегда показывать панель состояния"
|
||||
"Always Show Status Bar": "Всегда показывать панель состояния",
|
||||
"Translation Not Available": "Перевод недоступен"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Aramayı Temizle",
|
||||
"Header & Footer": "Başlık ve Altbilgi",
|
||||
"Apply also in Scrolled Mode": "Kaydırılmış Modda da Uygula",
|
||||
"A new version of Readest is Available!": "Yeni bir Readest sürümü mevcut!",
|
||||
"A new version of Readest is available!": "Yeni bir Readest sürümü mevcut!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} mevcut (yüklü sürüm {{currentVersion}}).",
|
||||
"Download and install now?": "Şimdi indirip yükleyelim mi?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
|
||||
@@ -363,5 +363,6 @@
|
||||
"Right Margin (px)": "Sağ Kenar Boşluğu (px)",
|
||||
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
|
||||
"Column Gap (%)": "Sütun Aralığı (%)",
|
||||
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster"
|
||||
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster",
|
||||
"Translation Not Available": "Çeviri Mevcut Değil"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Очистити пошук",
|
||||
"Header & Footer": "Заголовок і нижній колонтитул",
|
||||
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
|
||||
"A new version of Readest is Available!": "Доступна нова версія Readest!",
|
||||
"A new version of Readest is available!": "Доступна нова версія Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
|
||||
"Download and install now?": "Завантажити та встановити зараз?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
|
||||
@@ -369,5 +369,6 @@
|
||||
"Right Margin (px)": "Правий відступ (px)",
|
||||
"Left Margin (px)": "Лівий відступ (px)",
|
||||
"Column Gap (%)": "Проміжок між стовпцями (%)",
|
||||
"Always Show Status Bar": "Завжди показувати панель стану"
|
||||
"Always Show Status Bar": "Завжди показувати панель стану",
|
||||
"Translation Not Available": "Переклад недоступний"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "Xóa tìm kiếm",
|
||||
"Header & Footer": "Đầu trang & Chân trang",
|
||||
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
|
||||
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
|
||||
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
|
||||
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "Lề phải (px)",
|
||||
"Left Margin (px)": "Lề trái (px)",
|
||||
"Column Gap (%)": "Khoảng cách cột (%)",
|
||||
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái"
|
||||
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái",
|
||||
"Translation Not Available": "Không có bản dịch"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "页眉页脚",
|
||||
"Apply also in Scrolled Mode": "应用到滚动模式",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下载并安装?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "右边距",
|
||||
"Left Margin (px)": "左边距",
|
||||
"Column Gap (%)": "列间距",
|
||||
"Always Show Status Bar": "始终显示状态栏"
|
||||
"Always Show Status Bar": "始终显示状态栏",
|
||||
"Translation Not Available": "翻译不可用"
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
"Clear Search": "清除搜索",
|
||||
"Header & Footer": "頁眉頁腳",
|
||||
"Apply also in Scrolled Mode": "滾動模式同樣適用",
|
||||
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
|
||||
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
|
||||
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
|
||||
"Download and install now?": "立即下載並安裝?",
|
||||
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
|
||||
@@ -360,5 +360,6 @@
|
||||
"Right Margin (px)": "右邊距",
|
||||
"Left Margin (px)": "左邊距",
|
||||
"Column Gap (%)": "欄間距",
|
||||
"Always Show Status Bar": "始終顯示狀態欄"
|
||||
"Always Show Status Bar": "始終顯示狀態欄",
|
||||
"Translation Not Available": "翻譯不可用"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.60": {
|
||||
"date": "2025-06-22",
|
||||
"notes": [
|
||||
"Layout has been improved for iPad devices",
|
||||
"Screen orientation changes are now smoother on iOS",
|
||||
"Importing books on iOS during the first launch is now more reliable",
|
||||
"Font size no longer changes unexpectedly in scrolled mode on iOS",
|
||||
"Top and bottom safe insets are now ignored when no header or footer is present",
|
||||
"Interactive books now load correctly on the Desktop platform",
|
||||
"Translation is now disabled when the primary language is undefined",
|
||||
"Android: the task list background color has been updated with current theme color"
|
||||
]
|
||||
},
|
||||
"0.9.59": {
|
||||
"date": "2025-06-19",
|
||||
"notes": [
|
||||
"Layout: Image size now adjusts more accurately by converting screen units to pixels properly",
|
||||
"Layout: Improved header layout on iOS landscape mode — extra spacing has been removed",
|
||||
"Settings: Top and bottom margin settings are now more intuitive and better constrained"
|
||||
]
|
||||
},
|
||||
"0.9.58": {
|
||||
"date": "2025-06-18",
|
||||
"notes": [
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "❌ Failed to extract version from package.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Found version: $VERSION"
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
|
||||
|
||||
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
|
||||
echo "❌ Invalid version format: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert x.y.z => x * 10000 + y * 1000 + z
|
||||
VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
|
||||
echo "🔢 Computed versionCode: $VERSION_CODE"
|
||||
|
||||
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
|
||||
|
||||
if [[ ! -f "$PROPERTIES_FILE" ]]; then
|
||||
echo "❌ File not found: $PROPERTIES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
sed "s/^tauri\.android\.versionName=.*/tauri.android.versionName=$VERSION/" "$PROPERTIES_FILE" | \
|
||||
sed "s/^tauri\.android\.versionCode=.*/tauri.android.versionCode=$VERSION_CODE/" > "$tmpfile"
|
||||
mv "$tmpfile" "$PROPERTIES_FILE"
|
||||
|
||||
echo "✅ Updated $PROPERTIES_FILE"
|
||||
|
||||
echo "🚀 Running: pnpm tauri android build"
|
||||
pnpm tauri android build
|
||||
@@ -54,11 +54,14 @@
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.deepl.com"
|
||||
"url": "https://*.readest.com"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/readest/*"
|
||||
},
|
||||
{
|
||||
"url": "https://*.deepl.com"
|
||||
},
|
||||
{
|
||||
"url": "https://*.cloudflarestorage.com"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
|
||||
+38
@@ -1,10 +1,14 @@
|
||||
package com.bilingify.readest
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import android.view.KeyEvent
|
||||
import android.webkit.WebView
|
||||
import android.util.Log
|
||||
import android.graphics.Color
|
||||
import android.app.ActivityManager
|
||||
import android.content.res.Configuration
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -77,5 +81,39 @@ class MainActivity : TauriActivity(), KeyDownInterceptor {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
updateTaskDescription()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
updateTaskDescription()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateTaskDescription()
|
||||
}
|
||||
|
||||
private fun updateTaskDescription() {
|
||||
val backgroundColor = if (isDarkTheme()) {
|
||||
Color.parseColor("#2D2D2D")
|
||||
} else {
|
||||
Color.parseColor("#FFFFFF")
|
||||
}
|
||||
|
||||
setTaskDescription(
|
||||
ActivityManager.TaskDescription(
|
||||
"Readest",
|
||||
null,
|
||||
backgroundColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun isDarkTheme(): Boolean {
|
||||
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowSplashScreenBackground">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@null</item>
|
||||
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
|
||||
+107
-40
@@ -154,9 +154,8 @@ class VolumeKeyHandler: NSObject {
|
||||
class NativeBridgePlugin: Plugin {
|
||||
private var webView: WKWebView?
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
private var isOrientationLocked = false
|
||||
private var currentOrientationMask: UIInterfaceOrientationMask = .all
|
||||
private var orientationObserver: NSObjectProtocol?
|
||||
private var originalDelegate: UIApplicationDelegate?
|
||||
|
||||
@objc public override func load(webview: WKWebView) {
|
||||
self.webView = webview
|
||||
@@ -175,16 +174,19 @@ class NativeBridgePlugin: Plugin {
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
if let app = UIApplication.value(forKey: "sharedApplication") as? UIApplication {
|
||||
self.originalDelegate = app.delegate
|
||||
app.delegate = self
|
||||
} else {
|
||||
Logger.error("NativeBridgePlugin: Failed to get shared application")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidBecomeActive() {
|
||||
if volumeKeyHandler != nil {
|
||||
activateVolumeKeyInterception()
|
||||
}
|
||||
|
||||
if orientationObserver != nil {
|
||||
self.setupOrientationObserver()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
@@ -360,18 +362,11 @@ class NativeBridgePlugin: Plugin {
|
||||
let orientation = args.orientation ?? "auto"
|
||||
switch orientation.lowercased() {
|
||||
case "portrait":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .portrait
|
||||
self.forceOrientation(.portrait)
|
||||
self.setupOrientationObserver()
|
||||
self.changeOrientation(.portrait)
|
||||
case "landscape":
|
||||
self.isOrientationLocked = true
|
||||
self.currentOrientationMask = .landscape
|
||||
self.forceOrientation(.landscapeRight)
|
||||
self.setupOrientationObserver()
|
||||
self.changeOrientation(.landscape)
|
||||
case "auto":
|
||||
self.isOrientationLocked = false
|
||||
self.currentOrientationMask = .all
|
||||
self.changeOrientation(.all)
|
||||
default:
|
||||
invoke.reject("Invalid orientation mode")
|
||||
return
|
||||
@@ -381,40 +376,49 @@ class NativeBridgePlugin: Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private func forceOrientation(_ orientation: UIInterfaceOrientation) {
|
||||
private func changeOrientation(_ orientationMask: UIInterfaceOrientationMask) {
|
||||
self.currentOrientationMask = orientationMask
|
||||
if #available(iOS 16.0, *) {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
let orientationMask: UIInterfaceOrientationMask =
|
||||
orientation.isPortrait ? .portrait : .landscape
|
||||
|
||||
for window in windowScene.windows {
|
||||
if let rootVC = window.rootViewController {
|
||||
rootVC.setNeedsUpdateOfSupportedInterfaceOrientations()
|
||||
}
|
||||
}
|
||||
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
|
||||
print("Orientation update error: \(error.localizedDescription)")
|
||||
if orientationMask == .all {
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .all)) { error in
|
||||
logger.error("Orientation update error: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) { error in
|
||||
logger.error("Orientation update error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupOrientationObserver() {
|
||||
orientationObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.orientationDidChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
guard let self = self, self.isOrientationLocked else { return }
|
||||
|
||||
if self.currentOrientationMask == .portrait {
|
||||
self.forceOrientation(.portrait)
|
||||
} else if self.currentOrientationMask == .landscape {
|
||||
self.forceOrientation(.landscapeRight)
|
||||
if orientationMask == .all {
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
} else {
|
||||
let specificOrientation: UIInterfaceOrientation
|
||||
if orientationMask.contains(.portrait) {
|
||||
specificOrientation = .portrait
|
||||
} else if orientationMask.contains(.landscape) {
|
||||
let currentOrientation = UIDevice.current.orientation
|
||||
if currentOrientation == .landscapeLeft {
|
||||
specificOrientation = .landscapeRight
|
||||
} else if currentOrientation == .landscapeRight {
|
||||
specificOrientation = .landscapeLeft
|
||||
} else {
|
||||
specificOrientation = .landscapeRight
|
||||
}
|
||||
} else {
|
||||
specificOrientation = .portrait
|
||||
}
|
||||
UIDevice.current.setValue(specificOrientation.rawValue, forKey: "orientation")
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,3 +435,66 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
|
||||
return UIApplication.shared.windows.first ?? UIWindow()
|
||||
}
|
||||
}
|
||||
|
||||
extension NativeBridgePlugin: UIApplicationDelegate {
|
||||
public func application(
|
||||
_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?
|
||||
) -> UIInterfaceOrientationMask {
|
||||
return self.currentOrientationMask
|
||||
}
|
||||
|
||||
/*
|
||||
Proxy all application delegate methods to the original delegate:
|
||||
sel!(application:didFinishLaunchingWithOptions:),
|
||||
sel!(application:openURL:options:),
|
||||
sel!(application:continue:restorationHandler:),
|
||||
sel!(applicationDidBecomeActive:),
|
||||
sel!(applicationWillResignActive:),
|
||||
sel!(applicationWillEnterForeground:),
|
||||
sel!(applicationDidEnterBackground:),
|
||||
sel!(applicationWillTerminate:),
|
||||
*/
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
?? false
|
||||
}
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication, open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(application, open: url, options: options) ?? false
|
||||
}
|
||||
|
||||
public func application(
|
||||
_ application: UIApplication, continue continueUserActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||
) -> Bool {
|
||||
self.originalDelegate?.application?(
|
||||
application, continue: continueUserActivity, restorationHandler: restorationHandler) ?? false
|
||||
}
|
||||
|
||||
public func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationDidBecomeActive?(application)
|
||||
}
|
||||
|
||||
public func applicationWillResignActive(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillResignActive?(application)
|
||||
}
|
||||
|
||||
public func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillEnterForeground?(application)
|
||||
}
|
||||
|
||||
public func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationDidEnterBackground?(application)
|
||||
}
|
||||
|
||||
public func applicationWillTerminate(_ application: UIApplication) {
|
||||
self.originalDelegate?.applicationWillTerminate?(application)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,10 @@
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
|
||||
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
|
||||
"endpoints": [
|
||||
"https://download.readest.com/releases/latest.json",
|
||||
"https://github.com/readest/readest/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
return (
|
||||
<div className='hero bg-base-200 min-h-screen'>
|
||||
<div className='hero-content text-center'>
|
||||
<div className='max-w-2xl'>
|
||||
<div className='mb-8'>
|
||||
<div className='w-full max-w-2xl p-1'>
|
||||
<div className='mb-8 mt-6'>
|
||||
<div className='text-error animate-pulse text-8xl'>⚠️</div>
|
||||
</div>
|
||||
|
||||
@@ -55,8 +55,8 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
<p className='break-words font-mono text-sm'>{error.message}</p>
|
||||
{browserInfo && <p className='mt-2 font-mono text-sm'>Browser: {browserInfo}</p>}
|
||||
{error.stack && (
|
||||
<p className='mt-2 break-words font-mono text-sm'>
|
||||
{error.stack.split('\n').slice(0, 5).join('\n')}
|
||||
<p className='mt-2 whitespace-pre-wrap break-all font-mono text-sm'>
|
||||
{error.stack.split('\n').slice(0, 3).join('\n')}
|
||||
</p>
|
||||
)}
|
||||
{error.digest && <p className='mt-2 text-xs opacity-70'>Error ID: {error.digest}</p>}
|
||||
|
||||
@@ -102,6 +102,10 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</div>
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
style={{
|
||||
height: `${iconSize15}px`,
|
||||
minHeight: `${iconSize15}px`,
|
||||
}}
|
||||
>
|
||||
{book.progress && <ReadingProgress book={book} />}
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
|
||||
@@ -401,8 +401,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const exts = appService?.isMobileApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
|
||||
if (appService?.isIOSApp) {
|
||||
return files.filter((file) => {
|
||||
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
|
||||
return SUPPORTED_FILE_EXTS.includes(fileExt);
|
||||
});
|
||||
}
|
||||
// Cannot filter out files on Android since some content providers may not return the file name
|
||||
return files;
|
||||
};
|
||||
@@ -525,11 +531,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
let files;
|
||||
|
||||
if (isTauriAppPlatform()) {
|
||||
if (appService?.isIOSApp) {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
} else {
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
}
|
||||
files = (await selectFilesTauri()) as [string];
|
||||
} else {
|
||||
files = (await selectFilesWeb()) as [File];
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ interface BooksGridProps {
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
|
||||
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
|
||||
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
|
||||
@@ -45,6 +46,21 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sideBarBookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!screenInsets) return;
|
||||
bookKeys.forEach((bookKey, index) => {
|
||||
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
|
||||
const gridInsets = {
|
||||
top: top ? screenInsets.top : 0,
|
||||
right: right ? screenInsets.right : 0,
|
||||
bottom: bottom ? screenInsets.bottom : 0,
|
||||
left: left ? screenInsets.left : 0,
|
||||
};
|
||||
setGridInsets(bookKey, gridInsets);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKeys, screenInsets]);
|
||||
|
||||
if (!screenInsets) return null;
|
||||
|
||||
return (
|
||||
@@ -56,26 +72,18 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
}}
|
||||
>
|
||||
{bookKeys.map((bookKey, index) => {
|
||||
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
|
||||
const bookData = getBookData(bookKey);
|
||||
const config = getConfig(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const gridInsets = getGridInsets(bookKey);
|
||||
const { book, bookDoc } = bookData || {};
|
||||
if (!book || !config || !bookDoc || !viewSettings) return null;
|
||||
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
|
||||
|
||||
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
|
||||
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
|
||||
const horizontalGapPercent = viewSettings.gapPercent;
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
// insets for the grid cell
|
||||
const gridInsets = {
|
||||
top: top ? screenInsets.top : 0,
|
||||
right: right ? screenInsets.right : 0,
|
||||
bottom: bottom ? screenInsets.bottom : 0,
|
||||
left: left ? screenInsets.left : 0,
|
||||
};
|
||||
// insets for the content inside the grid cell
|
||||
const contentInsets = {
|
||||
top: gridInsets.top + viewInsets.top,
|
||||
right: gridInsets.right + viewInsets.right,
|
||||
@@ -104,12 +112,13 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
isHoveredAnim={bookKeys.length > 2}
|
||||
onCloseBook={onCloseBook}
|
||||
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
<FoliateViewer
|
||||
bookKey={bookKey}
|
||||
bookDoc={bookDoc}
|
||||
config={config}
|
||||
insets={contentInsets}
|
||||
contentInsets={contentInsets}
|
||||
/>
|
||||
{viewSettings.vertical && viewSettings.scrolled && (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { BookDoc, getDirection } from '@/libs/document';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { FoliateView, wrappedFoliateView } from '@/types/view';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
@@ -29,17 +30,25 @@ import {
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
eval(script: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
const FoliateViewer: React.FC<{
|
||||
bookKey: string;
|
||||
bookDoc: BookDoc;
|
||||
config: BookConfig;
|
||||
insets: { top: number; right: number; bottom: number; left: number };
|
||||
}> = ({ bookKey, bookDoc, config, insets }) => {
|
||||
contentInsets: Insets;
|
||||
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
|
||||
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getParallels } = useParallelViewStore();
|
||||
@@ -83,7 +92,7 @@ const FoliateViewer: React.FC<{
|
||||
.then((data) => {
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
if (viewSettings && detail.type === 'text/css')
|
||||
return transformStylesheet(viewSettings, width, height, data);
|
||||
return transformStylesheet(width, height, data);
|
||||
if (viewSettings && detail.type === 'application/xhtml+xml') {
|
||||
const ctx = {
|
||||
bookKey,
|
||||
@@ -120,6 +129,11 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
mountAdditionalFonts(detail.doc, isCJKLang(bookData.book?.primaryLanguage));
|
||||
|
||||
// Inline scripts in tauri platforms are not executed by default
|
||||
if (viewSettings.allowScript && isTauriAppPlatform()) {
|
||||
evalInlineScripts(detail.doc);
|
||||
}
|
||||
|
||||
// only call on load if we have highlighting turned on.
|
||||
if (viewSettings.codeHighlighting) {
|
||||
manageSyntaxHighlighting(detail.doc, viewSettings);
|
||||
@@ -142,6 +156,22 @@ const FoliateViewer: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const evalInlineScripts = (doc: Document) => {
|
||||
if (doc.defaultView && doc.defaultView.frameElement) {
|
||||
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
|
||||
const scripts = doc.querySelectorAll('script:not([src])');
|
||||
scripts.forEach((script, index) => {
|
||||
const scriptContent = script.textContent || script.innerHTML;
|
||||
try {
|
||||
console.warn('Evaluating inline scripts in iframe');
|
||||
iframe.contentWindow?.eval(scriptContent);
|
||||
} catch (error) {
|
||||
console.error(`Error executing iframe script ${index + 1}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const docRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
|
||||
@@ -206,9 +236,10 @@ const FoliateViewer: React.FC<{
|
||||
detail.allowScript = viewSettings.allowScript ?? false;
|
||||
}
|
||||
});
|
||||
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||
const width = containerRect?.width || window.innerWidth;
|
||||
const height = containerRect?.height || window.innerHeight;
|
||||
const viewWidth = appService?.isMobile ? screen.width : window.innerWidth;
|
||||
const viewHeight = appService?.isMobile ? screen.height : window.innerHeight;
|
||||
const width = viewWidth - insets.left - insets.right;
|
||||
const height = viewHeight - insets.top - insets.bottom;
|
||||
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
|
||||
view.renderer.setStyles?.(getStyles(viewSettings));
|
||||
|
||||
@@ -244,15 +275,25 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
const applyMarginAndGap = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
|
||||
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
|
||||
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
|
||||
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
|
||||
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
|
||||
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
|
||||
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
|
||||
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
|
||||
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
|
||||
viewRef.current?.renderer.setAttribute('margin-top', `${insets.top}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-right', `${insets.right + moreRightInset}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-bottom', `${insets.bottom}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-left', `${insets.left + moreLeftInset}px`);
|
||||
const topMargin = (showTopHeader ? insets.top : viewInsets.top) + moreTopInset;
|
||||
const rightMargin = insets.right + moreRightInset;
|
||||
const bottomMargin = (showBottomFooter ? insets.bottom : viewInsets.bottom) + moreBottomInset;
|
||||
const leftMargin = insets.left + moreLeftInset;
|
||||
|
||||
viewRef.current?.renderer.setAttribute('margin-top', `${topMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-right', `${rightMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-bottom', `${bottomMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('margin-left', `${leftMargin}px`);
|
||||
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
|
||||
if (viewSettings.scrolled) {
|
||||
viewRef.current?.renderer.setAttribute('flow', 'scrolled');
|
||||
|
||||
@@ -163,6 +163,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
? (progressInfo!.current + 1) / progressInfo!.total || 0
|
||||
: 0;
|
||||
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -202,7 +204,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
@@ -256,7 +258,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
@@ -298,7 +300,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom + 16}px` : '0px',
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -328,7 +330,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom / 2}px` : '0px',
|
||||
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
@@ -22,6 +23,7 @@ interface HeaderBarProps {
|
||||
bookTitle: string;
|
||||
isTopLeft: boolean;
|
||||
isHoveredAnim: boolean;
|
||||
gridInsets: Insets;
|
||||
onCloseBook: (bookKey: string) => void;
|
||||
onSetSettingsDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
@@ -31,6 +33,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
bookTitle,
|
||||
isTopLeft,
|
||||
isHoveredAnim,
|
||||
gridInsets,
|
||||
onCloseBook,
|
||||
onSetSettingsDialogOpen,
|
||||
}) => {
|
||||
@@ -77,10 +80,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 absolute top-0 w-full',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
className={clsx('bg-base-100 absolute top-0 w-full')}
|
||||
style={{
|
||||
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
|
||||
@@ -89,11 +92,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
|
||||
'bg-base-100 absolute left-0 right-0 top-0 z-10',
|
||||
isVisible ? 'visible' : 'hidden',
|
||||
)}
|
||||
style={{
|
||||
height: systemUIVisible ? `max(env(safe-area-inset-top), ${statusBarHeight}px)` : '',
|
||||
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -110,8 +113,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
)}
|
||||
style={{
|
||||
marginTop: systemUIVisible
|
||||
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
|
||||
: 'env(safe-area-inset-top)',
|
||||
? `${Math.max(gridInsets.top, statusBarHeight)}px`
|
||||
: `${gridInsets.top}px`,
|
||||
}}
|
||||
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
interface SectionInfoProps {
|
||||
@@ -22,6 +24,13 @@ const HintInfo: React.FC<SectionInfoProps> = ({
|
||||
contentInsets,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const topInset = Math.max(
|
||||
gridInsets.top,
|
||||
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
|
||||
);
|
||||
|
||||
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
|
||||
const hintTimeout = useRef(2000);
|
||||
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -57,7 +66,7 @@ const HintInfo: React.FC<SectionInfoProps> = ({
|
||||
hintMessage ? 'bg-base-100' : 'bg-transparent',
|
||||
)}
|
||||
style={{
|
||||
height: `${gridInsets.top}px`,
|
||||
height: `${topInset}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -83,7 +92,7 @@ const HintInfo: React.FC<SectionInfoProps> = ({
|
||||
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
|
||||
}
|
||||
: {
|
||||
top: `${gridInsets.top}px`,
|
||||
top: `${topInset}px`,
|
||||
insetInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
@@ -96,8 +96,9 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
useEffect(() => {
|
||||
if (!appService?.isMobileApp) return;
|
||||
const systemUIVisible = !!hoveredBookKey || settings.alwaysShowStatusBar;
|
||||
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
|
||||
if (systemUIVisible) {
|
||||
const visible = systemUIVisible && !systemUIAlwaysHidden;
|
||||
setSystemUIVisibility({ visible, darkMode: isDarkMode });
|
||||
if (visible) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
dismissSystemUI();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
|
||||
interface SectionInfoProps {
|
||||
section?: string;
|
||||
@@ -21,6 +23,13 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
contentInsets,
|
||||
gridInsets,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const topInset = Math.max(
|
||||
gridInsets.top,
|
||||
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -29,7 +38,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={{
|
||||
height: `${gridInsets.top}px`,
|
||||
height: `${topInset}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -49,7 +58,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
|
||||
}
|
||||
: {
|
||||
top: `${gridInsets.top}px`,
|
||||
top: `${topInset}px`,
|
||||
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
|
||||
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { saveViewSettings } from '../utils/viewSettingsHelper';
|
||||
import { isSameLang } from '@/utils/lang';
|
||||
import { isTranslationAvailable } from '@/services/translators/utils';
|
||||
import Button from '@/components/Button';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -19,9 +18,9 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
const bookData = getBookData(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
|
||||
|
||||
const primaryLanguage = bookData?.book?.primaryLanguage;
|
||||
const targetLanguage = viewSettings.translateTargetLang;
|
||||
const [translationAvailable, setTranslationAvailable] = useState(
|
||||
isTranslationAvailable(bookData?.book, viewSettings.translateTargetLang),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (translationEnabled === viewSettings.translationEnabled) return;
|
||||
@@ -34,19 +33,27 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
setTranslationEnabled(viewSettings.translationEnabled);
|
||||
setTranslationAvailable(
|
||||
isTranslationAvailable(bookData?.book, viewSettings.translateTargetLang),
|
||||
);
|
||||
}, [bookData, viewSettings.translationEnabled, viewSettings.translateTargetLang]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={
|
||||
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
|
||||
}
|
||||
disabled={
|
||||
!bookData ||
|
||||
bookData.book?.format === 'PDF' ||
|
||||
isSameLang(primaryLanguage, targetLanguage) ||
|
||||
(!targetLanguage && isSameLang(primaryLanguage, getLocale()))
|
||||
}
|
||||
disabled={!translationAvailable}
|
||||
onClick={() => setTranslationEnabled(!translationEnabled)}
|
||||
tooltip={translationEnabled ? _('Disable Translation') : _('Enable Translation')}
|
||||
tooltip={
|
||||
translationAvailable
|
||||
? translationEnabled
|
||||
? _('Disable Translation')
|
||||
: _('Enable Translation')
|
||||
: _('Translation Not Available')
|
||||
}
|
||||
tooltipDirection='bottom'
|
||||
></Button>
|
||||
);
|
||||
|
||||
@@ -18,11 +18,12 @@ import NumberInput from './NumberInput';
|
||||
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey)!;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const gridInsets = getGridInsets(bookKey)!;
|
||||
|
||||
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
|
||||
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
|
||||
@@ -98,42 +99,54 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (marginTopPx === viewSettings.marginTopPx) return;
|
||||
viewSettings.marginPx = undefined;
|
||||
if (viewSettings.marginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'marginTopPx', marginTopPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [marginTopPx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (marginBottomPx === viewSettings.marginBottomPx) return;
|
||||
viewSettings.marginPx = undefined;
|
||||
if (viewSettings.marginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'marginBottomPx', marginBottomPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [marginBottomPx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (marginRightPx === viewSettings.marginRightPx) return;
|
||||
viewSettings.marginPx = undefined;
|
||||
if (viewSettings.marginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'marginRightPx', marginRightPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [marginRightPx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (marginLeftPx === viewSettings.marginLeftPx) return;
|
||||
viewSettings.marginPx = undefined;
|
||||
if (viewSettings.marginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'marginLeftPx', marginLeftPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [marginLeftPx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (compactMarginTopPx === viewSettings.compactMarginTopPx) return;
|
||||
viewSettings.compactMarginPx = undefined;
|
||||
if (viewSettings.compactMarginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginTopPx', compactMarginTopPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [compactMarginTopPx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (compactMarginBottomPx === viewSettings.compactMarginBottomPx) return;
|
||||
viewSettings.compactMarginPx = undefined;
|
||||
if (viewSettings.compactMarginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(
|
||||
envConfig,
|
||||
bookKey,
|
||||
@@ -147,7 +160,9 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (compactMarginRightPx === viewSettings.compactMarginRightPx) return;
|
||||
viewSettings.compactMarginPx = undefined;
|
||||
if (viewSettings.compactMarginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(
|
||||
envConfig,
|
||||
bookKey,
|
||||
@@ -161,7 +176,9 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (compactMarginLeftPx === viewSettings.compactMarginLeftPx) return;
|
||||
viewSettings.compactMarginPx = undefined;
|
||||
if (viewSettings.compactMarginPx !== undefined) {
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
|
||||
}
|
||||
saveViewSettings(envConfig, bookKey, 'compactMarginLeftPx', compactMarginLeftPx, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [compactMarginLeftPx]);
|
||||
@@ -257,7 +274,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useEffect(() => {
|
||||
if (showHeader === viewSettings.showHeader) return;
|
||||
if (showHeader && !viewSettings.vertical) {
|
||||
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, 44);
|
||||
const minMarginTop = Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4);
|
||||
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, minMarginTop);
|
||||
setMarginTopPx(viewSettings.marginTopPx);
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
}
|
||||
@@ -269,7 +287,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useEffect(() => {
|
||||
if (showFooter === viewSettings.showFooter) return;
|
||||
if (showFooter && !viewSettings.vertical) {
|
||||
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, 44);
|
||||
const minMarginBottom = Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4);
|
||||
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, minMarginBottom);
|
||||
setMarginBottomPx(viewSettings.marginBottomPx);
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
}
|
||||
@@ -446,7 +465,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
label={_('Top Margin (px)')}
|
||||
value={showHeader && !isVertical ? marginTopPx : compactMarginTopPx}
|
||||
onChange={showHeader && !isVertical ? setMarginTopPx : setCompactMarginTopPx}
|
||||
min={0}
|
||||
min={
|
||||
showHeader && !isVertical
|
||||
? Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4)
|
||||
: 0
|
||||
}
|
||||
max={88}
|
||||
step={4}
|
||||
/>
|
||||
@@ -454,7 +477,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
label={_('Bottom Margin (px)')}
|
||||
value={showFooter && !isVertical ? marginBottomPx : compactMarginBottomPx}
|
||||
onChange={showFooter && !isVertical ? setMarginBottomPx : setCompactMarginBottomPx}
|
||||
min={0}
|
||||
min={
|
||||
showFooter && !isVertical
|
||||
? Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4)
|
||||
: 0
|
||||
}
|
||||
max={88}
|
||||
step={4}
|
||||
/>
|
||||
|
||||
@@ -133,7 +133,9 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
|
||||
>
|
||||
<div className='flex cursor-default items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<span className='text-gray-500'>{dayjs(item.createdAt).fromNow()}</span>
|
||||
<span className='text-sm text-gray-500 sm:text-xs'>
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center justify-end space-x-3 p-2' dir='ltr'>
|
||||
{item.note && (
|
||||
|
||||
@@ -177,6 +177,7 @@ const SideBar: React.FC<{
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 flex min-w-60 select-none flex-col',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
'transition-[padding-top] duration-300',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isSideBarPinned && 'shadow-2xl',
|
||||
|
||||
@@ -136,7 +136,6 @@ export function useTextTranslation(bookKey: string, view: FoliateView | HTMLElem
|
||||
wrapper.className = `translation-target ${!enabled.current ? 'hidden' : ''}`;
|
||||
wrapper.setAttribute('translation-element-mark', '1');
|
||||
wrapper.setAttribute('lang', targetLang || getLocale());
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
|
||||
const blockWrapper = document.createElement('font');
|
||||
blockWrapper.className = 'translation-target translation-block-wrapper';
|
||||
|
||||
@@ -47,7 +47,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent | CustomEvent) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
@@ -63,7 +63,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setIsFullHeightInMobile(!snapHeight);
|
||||
setIsFullHeightInMobile(!snapHeight && isMobile);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
if (appService?.isAndroidApp) {
|
||||
acquireBackKeyInterception();
|
||||
@@ -177,9 +177,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
appService?.hasSafeAreaInset && isFullHeightInMobile
|
||||
? `max(env(safe-area-inset-top), ${systemUIVisible ? statusBarHeight : 0}px)`
|
||||
: '0px',
|
||||
...(appService?.isMobile
|
||||
? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 }
|
||||
: {}),
|
||||
...(isMobile ? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 } : {}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -192,7 +190,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
|
||||
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-2 sm:px-4'>
|
||||
{header ? (
|
||||
header
|
||||
) : (
|
||||
|
||||
@@ -274,7 +274,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
|
||||
|
||||
<div className='text-base-content flex-grow text-sm'>
|
||||
<h2 className='mb-4 text-center font-bold sm:text-start'>
|
||||
{_('A new version of Readest is Available!')}
|
||||
{_('A new version of Readest is available!')}
|
||||
</h2>
|
||||
<p className='mb-2'>
|
||||
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
|
||||
|
||||
@@ -24,6 +24,8 @@ export const useTheme = ({
|
||||
dismissSystemUI,
|
||||
updateAppTheme,
|
||||
setStatusBarHeight,
|
||||
systemUIAlwaysHidden,
|
||||
setSystemUIAlwaysHidden,
|
||||
} = useThemeStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,12 +43,7 @@ export const useTheme = ({
|
||||
const handleSystemUIVisibility = useCallback(() => {
|
||||
if (!appService?.isMobileApp) return;
|
||||
|
||||
// This is a workaround for iPhone apps where the system UI is not visible in landscape mode
|
||||
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
|
||||
// in the ViewController.
|
||||
const isIPhoneApp = appService.isIOSApp && getOSPlatform() === 'ios';
|
||||
const systemUINeverVisible = isIPhoneApp && screen.orientation.type.includes('landscape');
|
||||
const visible = systemUIVisible && !systemUINeverVisible;
|
||||
const visible = systemUIVisible && !systemUIAlwaysHidden;
|
||||
if (visible) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
@@ -73,16 +70,20 @@ export const useTheme = ({
|
||||
}
|
||||
};
|
||||
const handleOrientationChange = () => {
|
||||
// Only handle orientation change for iPhone apps
|
||||
if (appService?.isIOSApp && getOSPlatform() === 'ios') {
|
||||
// FIXME: This is a workaround for iPhone apps where the system UI is not visible in landscape mode
|
||||
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
|
||||
// in the ViewController. Note that screen.orientation.type is not abailable in iOS before 16.4.
|
||||
const systemUIAlwaysHidden = screen.orientation?.type.includes('landscape');
|
||||
setSystemUIAlwaysHidden(systemUIAlwaysHidden);
|
||||
handleSystemUIVisibility();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
screen.orientation.addEventListener('change', handleOrientationChange);
|
||||
screen.orientation?.addEventListener('change', handleOrientationChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
screen.orientation.removeEventListener('change', handleOrientationChange);
|
||||
screen.orientation?.removeEventListener('change', handleOrientationChange);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleSystemUIVisibility]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Book, BookConfig, BookNote, BookDataRecord } from '@/types/book';
|
||||
import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { getAccessToken } from '@/utils/access';
|
||||
import { fetchWithTimeout } from '@/utils/fetch';
|
||||
|
||||
const SYNC_API_ENDPOINT = getAPIBaseUrl() + '/sync';
|
||||
|
||||
@@ -33,11 +34,15 @@ export class SyncClient {
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
|
||||
const url = `${SYNC_API_ENDPOINT}?since=${encodeURIComponent(since)}&type=${type ?? ''}&book=${book ?? ''}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
const res = await fetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
5000,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
@@ -55,14 +60,18 @@ export class SyncClient {
|
||||
const token = await getAccessToken();
|
||||
if (!token) throw new Error('Not authenticated');
|
||||
|
||||
const res = await fetch(SYNC_API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
const res = await fetchWithTimeout(
|
||||
SYNC_API_ENDPOINT,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
5000,
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
|
||||
@@ -89,7 +89,7 @@ export const DEFAULT_BOOK_FONT: BookFont = {
|
||||
|
||||
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
marginTopPx: 44,
|
||||
marginBottomPx: 16,
|
||||
marginBottomPx: 44,
|
||||
marginLeftPx: 16,
|
||||
marginRightPx: 16,
|
||||
compactMarginTopPx: 16,
|
||||
@@ -136,6 +136,7 @@ export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
fullJustification: false,
|
||||
animated: true,
|
||||
defaultFont: 'Sans-serif',
|
||||
marginBottomPx: 16,
|
||||
};
|
||||
|
||||
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
@@ -522,11 +523,11 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web'
|
||||
|
||||
export const READEST_WEB_BASE_URL = 'https://web.readest.com';
|
||||
|
||||
export const GITHUB_LATEST_DOWNLOAD = 'https://github.com/readest/readest/releases/latest/download';
|
||||
const LATEST_DOWNLOAD_BASE_URL = 'https://download.readest.com/releases';
|
||||
|
||||
export const READEST_UPDATER_FILE = `${GITHUB_LATEST_DOWNLOAD}/latest.json`;
|
||||
export const READEST_UPDATER_FILE = `${LATEST_DOWNLOAD_BASE_URL}/latest.json`;
|
||||
|
||||
export const READEST_CHANGELOG_FILE = `${GITHUB_LATEST_DOWNLOAD}/release-notes.json`;
|
||||
export const READEST_CHANGELOG_FILE = `${LATEST_DOWNLOAD_BASE_URL}/release-notes.json`;
|
||||
|
||||
export const SYNC_PROGRESS_INTERVAL_SEC = 3;
|
||||
export const SYNC_NOTES_INTERVAL_SEC = 5;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Book } from '@/types/book';
|
||||
import { isSameLang } from '@/utils/lang';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
||||
const DAILY_USAGE_KEY = 'translationDailyUsage';
|
||||
|
||||
export const saveDailyUsage = (usage: number, date?: string) => {
|
||||
@@ -21,3 +25,24 @@ export const getDailyUsage = (date?: string): number | null => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isTranslationAvailable = (book?: Book | null, targetLanguage?: string | null) => {
|
||||
if (!book || book.format === 'PDF') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const primaryLanguage = book.primaryLanguage || '';
|
||||
if (!primaryLanguage || primaryLanguage.toLowerCase() === 'und') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetLanguage && isSameLang(primaryLanguage, targetLanguage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!targetLanguage && isSameLang(primaryLanguage, getLocale())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ViewSettings,
|
||||
TimeInfo,
|
||||
} from '@/types/book';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { EnvConfigType } from '@/services/environment';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { DocumentLoader, TOCItem } from '@/libs/document';
|
||||
@@ -27,6 +28,7 @@ interface ViewState {
|
||||
progress: BookProgress | null;
|
||||
ribbonVisible: boolean;
|
||||
ttsEnabled: boolean;
|
||||
gridInsets: Insets | null;
|
||||
/* View settings for the view:
|
||||
generally view settings have a hierarchy of global settings < book settings < view settings
|
||||
view settings for primary view are saved to book config which is persisted to config file
|
||||
@@ -67,6 +69,8 @@ interface ReaderStore {
|
||||
) => Promise<void>;
|
||||
clearViewState: (key: string) => void;
|
||||
getViewState: (key: string) => ViewState | null;
|
||||
getGridInsets: (key: string) => Insets | null;
|
||||
setGridInsets: (key: string, insets: Insets | null) => void;
|
||||
}
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
@@ -114,6 +118,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
ttsEnabled: false,
|
||||
gridInsets: null,
|
||||
viewSettings: null,
|
||||
},
|
||||
},
|
||||
@@ -159,6 +164,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
ttsEnabled: false,
|
||||
gridInsets: null,
|
||||
viewSettings: { ...globalViewSettings, ...configViewSettings },
|
||||
},
|
||||
},
|
||||
@@ -178,6 +184,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
progress: null,
|
||||
ribbonVisible: false,
|
||||
ttsEnabled: false,
|
||||
gridInsets: null,
|
||||
viewSettings: null,
|
||||
},
|
||||
},
|
||||
@@ -306,4 +313,16 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
getGridInsets: (key: string) => get().viewStates[key]?.gridInsets || null,
|
||||
setGridInsets: (key: string, insets: Insets | null) =>
|
||||
set((state) => ({
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...state.viewStates[key]!,
|
||||
gridInsets: insets,
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -12,6 +12,8 @@ interface ThemeState {
|
||||
isDarkMode: boolean;
|
||||
systemUIVisible: boolean;
|
||||
statusBarHeight: number;
|
||||
systemUIAlwaysHidden: boolean;
|
||||
setSystemUIAlwaysHidden: (hidden: boolean) => void;
|
||||
setStatusBarHeight: (height: number) => void;
|
||||
showSystemUI: () => void;
|
||||
dismissSystemUI: () => void;
|
||||
@@ -69,9 +71,11 @@ export const useThemeStore = create<ThemeState>((set, get) => {
|
||||
themeCode,
|
||||
systemUIVisible: false,
|
||||
statusBarHeight: 24,
|
||||
systemUIAlwaysHidden: false,
|
||||
showSystemUI: () => set({ systemUIVisible: true }),
|
||||
dismissSystemUI: () => set({ systemUIVisible: false }),
|
||||
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
|
||||
setSystemUIAlwaysHidden: (hidden: boolean) => set({ systemUIAlwaysHidden: hidden }),
|
||||
getIsDarkMode: () => get().isDarkMode,
|
||||
setThemeMode: (mode) => {
|
||||
if (typeof window !== 'undefined' && localStorage) {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { getAccessToken } from './access';
|
||||
|
||||
export const fetchWithTimeout = (url: string, options: RequestInit, timeout = 8000) => {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(id));
|
||||
};
|
||||
|
||||
export const fetchWithAuth = async (url: string, options: RequestInit) => {
|
||||
const token = await getAccessToken();
|
||||
if (!token) {
|
||||
|
||||
@@ -31,7 +31,8 @@ export interface TextSelection {
|
||||
annotated?: boolean;
|
||||
}
|
||||
|
||||
const frameRect = (frame: Frame, rect: Rect, sx = 1, sy = 1) => {
|
||||
const frameRect = (frame: Frame, rect?: Rect, sx = 1, sy = 1) => {
|
||||
if (!rect) return { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
const left = sx * rect.left + frame.left;
|
||||
const right = sx * rect.right + frame.left;
|
||||
const top = sy * rect.top + frame.top;
|
||||
@@ -84,8 +85,8 @@ export const getPosition = (
|
||||
|
||||
const frame = frameElement?.getBoundingClientRect() ?? { top: 0, left: 0 };
|
||||
const rects = Array.from(target.getClientRects());
|
||||
const first = frameRect(frame, rects[0] as Rect, sx, sy);
|
||||
const last = frameRect(frame, rects.at(-1) as Rect, sx, sy);
|
||||
const first = frameRect(frame, rects[0], sx, sy);
|
||||
const last = frameRect(frame, rects.at(-1), sx, sy);
|
||||
|
||||
if (isVertical) {
|
||||
const leftSpace = first.left - rect.left;
|
||||
|
||||
@@ -58,6 +58,8 @@ const getFontStyles = (
|
||||
font-family: var(${defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'}) ${overrideFont ? '!important' : ''};
|
||||
font-size: ${fontSize}px !important;
|
||||
font-weight: ${fontWeight};
|
||||
-webkit-text-size-adjust: none;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
font[size="1"] {
|
||||
font-size: ${minFontSize}px;
|
||||
@@ -209,7 +211,7 @@ const getLayoutStyles = (
|
||||
width: auto;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
p, li, blockquote, dd, div:not(:has(*:not(b, a, em, i, strong, u, span))) {
|
||||
p, blockquote, dd, div:not(:has(*:not(b, a, em, i, strong, u, span))) {
|
||||
line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''};
|
||||
word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''};
|
||||
letter-spacing: ${letterSpacing}px ${overrideLayout ? '!important' : ''};
|
||||
@@ -240,6 +242,9 @@ const getLayoutStyles = (
|
||||
${!vertical && overrideLayout ? `margin-top: ${paragraphMargin}em !important;` : ''}
|
||||
${!vertical && overrideLayout ? `margin-bottom: ${paragraphMargin}em !important;` : ''}
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
:lang(zh), :lang(ja), :lang(ko) {
|
||||
widows: 1;
|
||||
@@ -420,16 +425,9 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
|
||||
return `${layoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
|
||||
};
|
||||
|
||||
export const transformStylesheet = (
|
||||
viewSettings: ViewSettings,
|
||||
width: number,
|
||||
height: number,
|
||||
css: string,
|
||||
) => {
|
||||
export const transformStylesheet = (vw: number, vh: number, css: string) => {
|
||||
const isMobile = ['ios', 'android'].includes(getOSPlatform());
|
||||
const fontScale = isMobile ? 1.25 : 1;
|
||||
const w = width - viewSettings.marginLeftPx - viewSettings.marginRightPx;
|
||||
const h = height - viewSettings.marginTopPx - viewSettings.marginBottomPx;
|
||||
const ruleRegex = /([^{]+)({[^}]+})/g;
|
||||
css = css.replace(ruleRegex, (match, selector, block) => {
|
||||
const hasTextAlignCenter = /text-align\s*:\s*center\s*[;$]/.test(block);
|
||||
@@ -467,8 +465,8 @@ export const transformStylesheet = (
|
||||
const rem = parseFloat(pt) / fontScale / 12;
|
||||
return `font-size: ${rem}rem`;
|
||||
})
|
||||
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * w) / 100 + 'px')
|
||||
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * h) / 100 + 'px')
|
||||
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * vw) / 100 + 'px')
|
||||
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * vh) / 100 + 'px')
|
||||
.replace(/[\s;]color\s*:\s*black/gi, 'color: var(--theme-fg-color)')
|
||||
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
|
||||
.replace(/[\s;]color\s*:\s*#000/gi, 'color: var(--theme-fg-color)')
|
||||
|
||||
@@ -69,7 +69,7 @@ export const webDownload = async (downloadUrl: string, onProgress?: ProgressHand
|
||||
}
|
||||
}
|
||||
|
||||
return new Blob(chunks);
|
||||
return new Blob(chunks as BlobPart[]);
|
||||
};
|
||||
|
||||
export const tauriUpload = async (
|
||||
|
||||
Reference in New Issue
Block a user