forked from akai/readest
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cb81b3795 | |||
| 91716df5e0 | |||
| f5f07e9392 | |||
| 7e385db4ed | |||
| 1b99915663 | |||
| 0a213d6a0e | |||
| f9e9c90386 | |||
| 8b2888af34 | |||
| ffb00f4e0d | |||
| 77da5423a6 | |||
| ee201d4571 | |||
| e48cfc407c | |||
| d49be1f9ca | |||
| c46cf5b0bf | |||
| 3078b09ba4 | |||
| 05cb53ede4 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -26,7 +26,10 @@
|
||||
"build-macos-universial": "dotenv -e .env.tauri.local -e .env.apple-nonstore.local -- tauri build -t universal-apple-darwin --bundles dmg",
|
||||
"build-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore.conf.json",
|
||||
"build-macos-universial-appstore-dev": "dotenv -e .env.tauri.local -e .env.apple-appstore-dev.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore-dev.conf.json",
|
||||
"build-ios": "dotenv -e .env.ios-appstore-dev.local -- tauri ios build",
|
||||
"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",
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler dev",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler deploy",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Notizbuch",
|
||||
"Notes": "Notizen",
|
||||
"Open": "Öffnen",
|
||||
"Open Book": "Buch öffnen",
|
||||
"Original Text": "Originaltext",
|
||||
"Page": "Seite",
|
||||
"Paging Animation": "Blätter-Animation",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Unbenannte Gruppe",
|
||||
"Group Books": "Bücher gruppieren",
|
||||
"Remove From Group": "Aus Gruppe entfernen",
|
||||
"Create New Group": "Neue Gruppe erstellen"
|
||||
"Create New Group": "Neue Gruppe erstellen",
|
||||
"Deselect Book": "Buch abwählen",
|
||||
"Download Book": "Buch herunterladen",
|
||||
"Deselect Group": "Gruppe abwählen",
|
||||
"Select Group": "Gruppe auswählen",
|
||||
"Keep Screen Awake": "Bildschirm aktiv halten"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Σημειωματάριο",
|
||||
"Notes": "Σημειώσεις",
|
||||
"Open": "Άνοιγμα",
|
||||
"Open Book": "Άνοιγμα βιβλίου",
|
||||
"Original Text": "Αρχικό κείμενο",
|
||||
"Page": "Σελίδα",
|
||||
"Paging Animation": "Κινούμενα σχέδια σελιδοποίησης",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Ομάδα χωρίς τίτλο",
|
||||
"Group Books": "Ομαδοποίηση βιβλίων",
|
||||
"Remove From Group": "Αφαίρεση από την ομάδα",
|
||||
"Create New Group": "Δημιουργία νέας ομάδας"
|
||||
"Create New Group": "Δημιουργία νέας ομάδας",
|
||||
"Deselect Book": "Αποεπιλογή βιβλίου",
|
||||
"Download Book": "Λήψη βιβλίου",
|
||||
"Deselect Group": "Αποεπιλογή ομάδας",
|
||||
"Select Group": "Επιλογή ομάδας",
|
||||
"Keep Screen Awake": "Διατήρηση ενεργού οθόνης"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Cuaderno",
|
||||
"Notes": "Notas",
|
||||
"Open": "Abrir",
|
||||
"Open Book": "Abrir libro",
|
||||
"Original Text": "Texto original",
|
||||
"Page": "Página",
|
||||
"Paging Animation": "Animación de paginación",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Grupo sin título",
|
||||
"Group Books": "Agrupar libros",
|
||||
"Remove From Group": "Eliminar del grupo",
|
||||
"Create New Group": "Crear nuevo grupo"
|
||||
"Create New Group": "Crear nuevo grupo",
|
||||
"Deselect Book": "Deseleccionar libro",
|
||||
"Download Book": "Descargar libro",
|
||||
"Deselect Group": "Deseleccionar grupo",
|
||||
"Select Group": "Seleccionar grupo",
|
||||
"Keep Screen Awake": "Mantener pantalla activa"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Carnet de notes",
|
||||
"Notes": "Notes",
|
||||
"Open": "Ouvrir",
|
||||
"Open Book": "Ouvrir le livre",
|
||||
"Original Text": "Texte original",
|
||||
"Page": "Page",
|
||||
"Paging Animation": "Animation de page",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Groupe sans titre",
|
||||
"Group Books": "Regrouper les livres",
|
||||
"Remove From Group": "Retirer du groupe",
|
||||
"Create New Group": "Créer un nouveau groupe"
|
||||
"Create New Group": "Créer un nouveau groupe",
|
||||
"Deselect Book": "Désélectionner le livre",
|
||||
"Download Book": "Télécharger le livre",
|
||||
"Deselect Group": "Désélectionner le groupe",
|
||||
"Select Group": "Sélectionner le groupe",
|
||||
"Keep Screen Awake": "Maintenir l'écran allumé"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "नोटबुक",
|
||||
"Notes": "नोट्स",
|
||||
"Open": "खोलें",
|
||||
"Open Book": "पुस्तक खोलें",
|
||||
"Original Text": "मूल पाठ",
|
||||
"Page": "पृष्ठ",
|
||||
"Paging Animation": "पेजिंग एनीमेशन",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "अभिनामित समूह",
|
||||
"Group Books": "समूह पुस्तकें",
|
||||
"Remove From Group": "समूह से हटाएं",
|
||||
"Create New Group": "नया समूह बनाएं"
|
||||
"Create New Group": "नया समूह बनाएं",
|
||||
"Deselect Book": "पुस्तक का चयन रद्द करें",
|
||||
"Download Book": "पुस्तक डाउनलोड करें",
|
||||
"Deselect Group": "समूह का चयन रद्द करें",
|
||||
"Select Group": "समूह चुनें",
|
||||
"Keep Screen Awake": "स्क्रीन जागरूक रखें"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Buku Catatan",
|
||||
"Notes": "Catatan",
|
||||
"Open": "Buka",
|
||||
"Open Book": "Buka Buku",
|
||||
"Original Text": "Teks Asli",
|
||||
"Page": "Halaman",
|
||||
"Paging Animation": "Animasi Halaman",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Grup Tanpa Judul",
|
||||
"Group Books": "Grupkan Buku",
|
||||
"Remove From Group": "Hapus dari Grup",
|
||||
"Create New Group": "Buat Grup Baru"
|
||||
"Create New Group": "Buat Grup Baru",
|
||||
"Deselect Book": "Batalkan Pilihan Buku",
|
||||
"Download Book": "Unduh Buku",
|
||||
"Deselect Group": "Batalkan Pilihan Grup",
|
||||
"Select Group": "Pilih Grup",
|
||||
"Keep Screen Awake": "Biarkan Layar Tetap Hidup"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Quaderno",
|
||||
"Notes": "Note",
|
||||
"Open": "Apri",
|
||||
"Open Book": "Apri libro",
|
||||
"Original Text": "Testo originale",
|
||||
"Page": "Pagina",
|
||||
"Paging Animation": "Animazione cambio pagina",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Gruppo senza titolo",
|
||||
"Group Books": "Raggruppa libri",
|
||||
"Remove From Group": "Rimuovi dal",
|
||||
"Create New Group": "Crea nuovo gruppo"
|
||||
"Create New Group": "Crea nuovo gruppo",
|
||||
"Deselect Book": "Deseleziona libro",
|
||||
"Download Book": "Scarica libro",
|
||||
"Deselect Group": "Deseleziona gruppo",
|
||||
"Select Group": "Seleziona gruppo",
|
||||
"Keep Screen Awake": "Mantieni schermo attivo"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "ノート",
|
||||
"Notes": "メモ",
|
||||
"Open": "開く",
|
||||
"Open Book": "書籍を開く",
|
||||
"Original Text": "原文",
|
||||
"Page": "ページ",
|
||||
"Paging Animation": "ページめくりアニメーション",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "無題のグループ",
|
||||
"Group Books": "書籍をグループ化",
|
||||
"Remove From Group": "グループから削除",
|
||||
"Create New Group": "新しいグループを作成"
|
||||
"Create New Group": "新しいグループを作成",
|
||||
"Deselect Book": "書籍の選択を解除",
|
||||
"Download Book": "書籍をダウンロード",
|
||||
"Deselect Group": "グループの選択を解除",
|
||||
"Select Group": "グループを選択",
|
||||
"Keep Screen Awake": "画面をオンのままにする"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "노트북",
|
||||
"Notes": "메모",
|
||||
"Open": "열기",
|
||||
"Open Book": "책 열기",
|
||||
"Original Text": "원본 텍스트",
|
||||
"Page": "페이지",
|
||||
"Paging Animation": "페이지 넘김 애니메이션",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "제목 없는 그룹",
|
||||
"Group Books": "책 그룹화",
|
||||
"Remove From Group": "그룹에서 제거",
|
||||
"Create New Group": "새 그룹 만들기"
|
||||
"Create New Group": "새 그룹 만들기",
|
||||
"Deselect Book": "책 선택 해제",
|
||||
"Download Book": "책 다운로드",
|
||||
"Deselect Group": "그룹 선택 해제",
|
||||
"Select Group": "그룹 선택",
|
||||
"Keep Screen Awake": "화면 켜짐 유지"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Notatnik",
|
||||
"Notes": "Notatki",
|
||||
"Open": "Otwórz",
|
||||
"Open Book": "Otwórz książkę",
|
||||
"Original Text": "Tekst oryginalny",
|
||||
"Page": "Strona",
|
||||
"Paging Animation": "Animacja przewracania stron",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Grupa bez tytułu",
|
||||
"Group Books": "Grupuj książki",
|
||||
"Remove From Group": "Usuń z grupy",
|
||||
"Create New Group": "Utwórz nową grupę"
|
||||
"Create New Group": "Utwórz nową grupę",
|
||||
"Deselect Book": "Odznacz książkę",
|
||||
"Download Book": "Pobierz książkę",
|
||||
"Deselect Group": "Odznacz grupę",
|
||||
"Select Group": "Wybierz grupę",
|
||||
"Keep Screen Awake": "Nie wygaszaj ekranu"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Caderno",
|
||||
"Notes": "Notas",
|
||||
"Open": "Abrir",
|
||||
"Open Book": "Abrir Livro",
|
||||
"Original Text": "Texto Original",
|
||||
"Page": "Página",
|
||||
"Paging Animation": "Animação de Página",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Grupo Sem Título",
|
||||
"Group Books": "Agrupar Livros",
|
||||
"Remove From Group": "Remover do Grupo",
|
||||
"Create New Group": "Criar Novo Grupo"
|
||||
"Create New Group": "Criar Novo Grupo",
|
||||
"Deselect Book": "Desmarcar Livro",
|
||||
"Download Book": "Baixar Livro",
|
||||
"Deselect Group": "Desmarcar Grupo",
|
||||
"Select Group": "Selecionar Grupo",
|
||||
"Keep Screen Awake": "Manter a Tela Ativa"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Блокнот",
|
||||
"Notes": "Заметки",
|
||||
"Open": "Открыть",
|
||||
"Open Book": "Открыть книгу",
|
||||
"Original Text": "Оригинальный текст",
|
||||
"Page": "Страница",
|
||||
"Paging Animation": "Анимация перелистывания",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Группа без названия",
|
||||
"Group Books": "Группировать книги",
|
||||
"Remove From Group": "Удалить из группы",
|
||||
"Create New Group": "Создать новую группу"
|
||||
"Create New Group": "Создать новую группу",
|
||||
"Deselect Book": "Отменить выбор книги",
|
||||
"Download Book": "Скачать книгу",
|
||||
"Deselect Group": "Отменить выбор группы",
|
||||
"Select Group": "Выбрать группу",
|
||||
"Keep Screen Awake": "Не выключать экран"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Not Defteri",
|
||||
"Notes": "Notlar",
|
||||
"Open": "Aç",
|
||||
"Open Book": "Kitabı Aç",
|
||||
"Original Text": "Orijinal Metin",
|
||||
"Page": "Sayfa",
|
||||
"Paging Animation": "Sayfa Çevirme Animasyonu",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Başlıksız Grup",
|
||||
"Group Books": "Kitapları Grupla",
|
||||
"Remove From Group": "Grubu Kaldır",
|
||||
"Create New Group": "Yeni Grup Oluştur"
|
||||
"Create New Group": "Yeni Grup Oluştur",
|
||||
"Deselect Book": "Kitabı Seçme",
|
||||
"Download Book": "Kitabı İndir",
|
||||
"Deselect Group": "Grubu Seçme",
|
||||
"Select Group": "Grup Seç",
|
||||
"Keep Screen Awake": "Ekranı Açık Tut"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Нотатник",
|
||||
"Notes": "Нотатки",
|
||||
"Open": "Відкрити",
|
||||
"Open Book": "Відкрити книгу",
|
||||
"Original Text": "Оригінальний текст",
|
||||
"Page": "Сторінка",
|
||||
"Paging Animation": "Анімація гортання",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Група без назви",
|
||||
"Group Books": "Групувати книги",
|
||||
"Remove From Group": "Видалити з групи",
|
||||
"Create New Group": "Створити нову групу"
|
||||
"Create New Group": "Створити нову групу",
|
||||
"Deselect Book": "Скасувати вибір книги",
|
||||
"Download Book": "Завантажити книгу",
|
||||
"Deselect Group": "Скасувати вибір групи",
|
||||
"Select Group": "Вибрати групу",
|
||||
"Keep Screen Awake": "Не вимикати екран"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "Sổ ghi chép",
|
||||
"Notes": "Ghi chú",
|
||||
"Open": "Mở",
|
||||
"Open Book": "Mở sách",
|
||||
"Original Text": "Văn bản gốc",
|
||||
"Page": "Trang",
|
||||
"Paging Animation": "Hiệu ứng lật trang",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "Nhóm không có tiêu đề",
|
||||
"Group Books": "Nhóm sách",
|
||||
"Remove From Group": "Xóa khỏi nhóm",
|
||||
"Create New Group": "Tạo nhóm mới"
|
||||
"Create New Group": "Tạo nhóm mới",
|
||||
"Deselect Book": "Bỏ chọn sách",
|
||||
"Download Book": "Tải sách",
|
||||
"Deselect Group": "Bỏ chọn nhóm",
|
||||
"Select Group": "Chọn nhóm",
|
||||
"Keep Screen Awake": "Giữ màn hình luôn bật"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "笔记本",
|
||||
"Notes": "笔记",
|
||||
"Open": "打开",
|
||||
"Open Book": "打开书籍",
|
||||
"Original Text": "原文",
|
||||
"Page": "页面",
|
||||
"Paging Animation": "翻页动画",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "无标题分组",
|
||||
"Group Books": "分组书籍",
|
||||
"Remove From Group": "从分组中移除",
|
||||
"Create New Group": "创建新分组"
|
||||
"Create New Group": "创建新分组",
|
||||
"Deselect Book": "取消选择书籍",
|
||||
"Download Book": "下载书籍",
|
||||
"Deselect Group": "取消选择分组",
|
||||
"Select Group": "选择分组",
|
||||
"Keep Screen Awake": "保持屏幕常亮"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"Notebook": "筆記本",
|
||||
"Notes": "筆記",
|
||||
"Open": "打開",
|
||||
"Open Book": "打開書籍",
|
||||
"Original Text": "原文",
|
||||
"Page": "頁面",
|
||||
"Paging Animation": "翻頁動畫",
|
||||
@@ -161,5 +160,10 @@
|
||||
"Untitled Group": "無標題組",
|
||||
"Group Books": "分組書籍",
|
||||
"Remove From Group": "從組中移除",
|
||||
"Create New Group": "創建新組"
|
||||
"Create New Group": "創建新組",
|
||||
"Deselect Book": "取消選擇書籍",
|
||||
"Download Book": "下載書籍",
|
||||
"Deselect Group": "取消選擇組",
|
||||
"Select Group": "選擇組",
|
||||
"Keep Screen Awake": "保持屏幕常亮"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.13": {
|
||||
"date": "2025-02-19",
|
||||
"notes": [
|
||||
"Add option to keep screen awake when reading",
|
||||
"Add shortcuts to notetaking UI",
|
||||
"UX enhancements of grid view in mobile platforms"
|
||||
]
|
||||
},
|
||||
"0.9.11": {
|
||||
"date": "2025-02-14",
|
||||
"notes": [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
pnpm tauri ios build --export-method app-store-connect
|
||||
|
||||
BUNDLE_DIR=src-tauri/gen/apple/build/arm64
|
||||
IPA_BUNDLE=$BUNDLE_DIR/Readest.ipa
|
||||
|
||||
xcrun altool --upload-app --type ios --file $IPA_BUNDLE --apiKey $APPLE_API_KEY --apiIssuer $APPLE_API_ISSUER
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 720 KiB After Width: | Height: | Size: 611 KiB |
@@ -16,10 +16,9 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { onOpenUrl, getCurrent } from '@tauri-apps/plugin-deep-link';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { tauriHandleOnWindowFocus } from '@/utils/window';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
@@ -37,6 +36,9 @@ interface ProviderLoginProp {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const WEB_AUTH_CALLBACK = 'https://web.readest.com/auth/callback';
|
||||
const DEEPLINK_CALLBACK = 'readest://auth/callback';
|
||||
|
||||
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
|
||||
return (
|
||||
<button
|
||||
@@ -66,8 +68,8 @@ export default function AuthPage() {
|
||||
const getTauriRedirectTo = () => {
|
||||
return process.env.NODE_ENV === 'production'
|
||||
? ['android', 'ios'].includes(osPlatform)
|
||||
? 'https://web.readest.com/auth/callback'
|
||||
: 'readest://auth/callback'
|
||||
? WEB_AUTH_CALLBACK
|
||||
: DEEPLINK_CALLBACK
|
||||
: `http://localhost:${port}`;
|
||||
};
|
||||
|
||||
@@ -88,22 +90,7 @@ export default function AuthPage() {
|
||||
console.error('Authentication error:', error);
|
||||
return;
|
||||
}
|
||||
openUrl(data.url);
|
||||
|
||||
// FIXME: For Android we need a better way to trigger the deeplink redirect
|
||||
if (isTauriAppPlatform() && osPlatform === 'android') {
|
||||
tauriHandleOnWindowFocus(async () => {
|
||||
const urls = await getCurrent();
|
||||
if (urls && urls.length > 0) {
|
||||
urls.forEach((url) => {
|
||||
handleOAuthUrl(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.back();
|
||||
}, 5000);
|
||||
}
|
||||
await openUrl(data.url);
|
||||
};
|
||||
|
||||
const handleOAuthUrl = async (url: string) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ interface BookItemProps {
|
||||
handleBookUpload: (book: Book) => void;
|
||||
handleBookDownload: (book: Book) => void;
|
||||
showBookDetailsModal: (book: Book) => void;
|
||||
bookContextMenuHandler: (book: Book, e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const BookItem: React.FC<BookItemProps> = ({
|
||||
@@ -28,7 +27,6 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
handleBookUpload,
|
||||
handleBookDownload,
|
||||
showBookDetailsModal,
|
||||
bookContextMenuHandler,
|
||||
}) => {
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const { appService } = useEnv();
|
||||
@@ -44,9 +42,8 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
'book-item flex h-full flex-col',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
onContextMenu={bookContextMenuHandler.bind(null, book)}
|
||||
>
|
||||
<div className='bg-base-100 relative aspect-[28/41] shadow-md'>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md'>
|
||||
<BookCover book={book} />
|
||||
{selectedBooks.includes(book.hash) && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
@@ -89,10 +86,11 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
) : (
|
||||
<button
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onTouchStart={(e) => stopEvent(e)}
|
||||
onMouseDown={(e) => stopEvent(e)}
|
||||
onTouchEnd={(e) => stopEvent(e)}
|
||||
onMouseUp={(e) => stopEvent(e)}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => {
|
||||
if (!book.uploadedAt) {
|
||||
handleBookUpload(book);
|
||||
@@ -109,10 +107,11 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
)}
|
||||
<button
|
||||
className='show-detail-button opacity-0 group-hover:opacity-100'
|
||||
onTouchStart={(e) => stopEvent(e)}
|
||||
onMouseDown={(e) => stopEvent(e)}
|
||||
onTouchEnd={(e) => stopEvent(e)}
|
||||
onMouseUp={(e) => stopEvent(e)}
|
||||
onPointerDown={(e) => stopEvent(e)}
|
||||
onPointerUp={(e) => stopEvent(e)}
|
||||
onPointerMove={(e) => stopEvent(e)}
|
||||
onPointerCancel={(e) => stopEvent(e)}
|
||||
onPointerLeave={(e) => stopEvent(e)}
|
||||
onClick={() => showBookDetailsModal(book)}
|
||||
>
|
||||
<CiCircleMore size={iconSize15} />
|
||||
|
||||
@@ -100,7 +100,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
navigateToLibrary(router);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams, showGroupingModal]);
|
||||
}, [searchParams, libraryBooks, showGroupingModal]);
|
||||
|
||||
const toggleSelection = (id: string) => {
|
||||
setSelectedBooks((prev) =>
|
||||
@@ -138,7 +138,12 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
|
||||
return (
|
||||
<div className='bookshelf'>
|
||||
<div className='transform-wrapper grid flex-1 grid-cols-3 gap-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
|
||||
<div
|
||||
className={clsx(
|
||||
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
|
||||
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8',
|
||||
)}
|
||||
>
|
||||
{currentBookshelfItems.map((item, index) => (
|
||||
<BookshelfItem
|
||||
key={`library-item-${index}`}
|
||||
@@ -159,7 +164,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
))}
|
||||
{!navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<div
|
||||
className='border-1 bg-base-100 hover:bg-base-300/50 m-4 flex aspect-[28/41] items-center justify-center'
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
|
||||
'mx-0 my-4 aspect-[28/41] sm:mx-4',
|
||||
)}
|
||||
role='button'
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
@@ -241,7 +249,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
{showDeleteAlert && (
|
||||
<div
|
||||
className={clsx(
|
||||
'fixed bottom-0 z-50 flex justify-center',
|
||||
'fixed bottom-0 left-0 right-0 z-50 flex justify-center',
|
||||
'pb-[calc(env(safe-area-inset-bottom)+16px)]',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -107,11 +107,14 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const makeBookAvailable = async (book: Book) => {
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
let available = false;
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 200);
|
||||
try {
|
||||
await handleBookDownload(book);
|
||||
updateBook(envConfig, book);
|
||||
available = true;
|
||||
} finally {
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
return available;
|
||||
}
|
||||
}
|
||||
@@ -119,12 +122,10 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
};
|
||||
|
||||
const handleBookClick = async (book: Book) => {
|
||||
if (!(await makeBookAvailable(book))) return;
|
||||
|
||||
if (isSelectMode) {
|
||||
toggleSelection(book.hash);
|
||||
} else {
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
if (!(await makeBookAvailable(book))) return;
|
||||
navigateToReader(router, [book.hash]);
|
||||
}
|
||||
};
|
||||
@@ -137,24 +138,16 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemContextMenu = (item: BookshelfItem, event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if ('format' in item) {
|
||||
bookContextMenuHandler(item as Book, event);
|
||||
}
|
||||
};
|
||||
|
||||
const bookContextMenuHandler = async (book: Book, e: React.MouseEvent) => {
|
||||
const bookContextMenuHandler = async (book: Book) => {
|
||||
if (!appService?.hasContextMenu) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const osPlatform = getOSPlatform();
|
||||
const fileRevealLabel =
|
||||
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
|
||||
const openBookMenuItem = await MenuItem.new({
|
||||
text: isSelectMode ? _('Select Book') : _('Open Book'),
|
||||
const selectBookMenuItem = await MenuItem.new({
|
||||
text: selectedBooks.includes(book.hash) ? _('Deselect Book') : _('Select Book'),
|
||||
action: async () => {
|
||||
handleBookClick(book);
|
||||
if (!isSelectMode) handleSetSelectMode(true);
|
||||
toggleSelection(book.hash);
|
||||
},
|
||||
});
|
||||
const showBookInFinderMenuItem = await MenuItem.new({
|
||||
@@ -170,6 +163,12 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
showBookDetailsModal(book);
|
||||
},
|
||||
});
|
||||
const downloadBookMenuItem = await MenuItem.new({
|
||||
text: _('Download Book'),
|
||||
action: async () => {
|
||||
handleBookDownload(book);
|
||||
},
|
||||
});
|
||||
const uploadBookMenuItem = await MenuItem.new({
|
||||
text: _('Upload Book'),
|
||||
action: async () => {
|
||||
@@ -183,14 +182,42 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
},
|
||||
});
|
||||
const menu = await Menu.new();
|
||||
menu.append(openBookMenuItem);
|
||||
menu.append(selectBookMenuItem);
|
||||
menu.append(showBookDetailsMenuItem);
|
||||
menu.append(showBookInFinderMenuItem);
|
||||
menu.append(uploadBookMenuItem);
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
menu.append(downloadBookMenuItem);
|
||||
}
|
||||
if (!book.uploadedAt && book.downloadedAt) {
|
||||
menu.append(uploadBookMenuItem);
|
||||
}
|
||||
menu.append(deleteBookMenuItem);
|
||||
menu.popup();
|
||||
};
|
||||
|
||||
const groupContextMenuHandler = async (group: BooksGroup) => {
|
||||
if (!appService?.hasContextMenu) return;
|
||||
const selectGroupMenuItem = await MenuItem.new({
|
||||
text: selectedBooks.includes(group.id) ? _('Deselect Group') : _('Select Group'),
|
||||
action: async () => {
|
||||
if (!isSelectMode) handleSetSelectMode(true);
|
||||
toggleSelection(group.id);
|
||||
},
|
||||
});
|
||||
const deleteGroupMenuItem = await MenuItem.new({
|
||||
text: _('Delete'),
|
||||
action: async () => {
|
||||
for (const book of group.books) {
|
||||
await handleBookDelete(book);
|
||||
}
|
||||
},
|
||||
});
|
||||
const menu = await Menu.new();
|
||||
menu.append(selectGroupMenuItem);
|
||||
menu.append(deleteGroupMenuItem);
|
||||
menu.popup();
|
||||
};
|
||||
|
||||
const { pressing, handlers } = useLongPress({
|
||||
onLongPress: () => {
|
||||
if (!isSelectMode) {
|
||||
@@ -209,19 +236,25 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
handleGroupClick(item as BooksGroup);
|
||||
}
|
||||
},
|
||||
onContextMenu: () => {
|
||||
if ('format' in item) {
|
||||
bookContextMenuHandler(item as Book);
|
||||
} else {
|
||||
groupContextMenuHandler(item as BooksGroup);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'hover:bg-base-300/50 group flex h-full flex-col p-4',
|
||||
'sm:hover:bg-base-300/50 group flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
pressing ? 'scale-95' : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
{...handlers}
|
||||
onContextMenu={(event) => handleItemContextMenu(item, event)}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
@@ -229,11 +262,10 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
book={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
bookContextMenuHandler={bookContextMenuHandler}
|
||||
transferProgress={transferProgress}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BooksGroup } from '@/types/book';
|
||||
import BookCover from './BookCover';
|
||||
|
||||
@@ -9,9 +11,15 @@ interface GroupItemProps {
|
||||
}
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBooks }) => {
|
||||
const { appService } = useEnv();
|
||||
return (
|
||||
<div className='group-item flex h-full flex-col'>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center p-2 shadow-md'>
|
||||
<div
|
||||
className={clsx(
|
||||
'group-item flex h-full flex-col',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden p-2 shadow-md'>
|
||||
<div className='grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden'>
|
||||
{group.books.slice(0, 4).map((book) => (
|
||||
<div key={book.hash} className='relative aspect-[28/41] h-full w-full'>
|
||||
|
||||
@@ -49,16 +49,16 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
};
|
||||
}, [onToggleSelectMode]);
|
||||
|
||||
const windowButtonVisible = appService?.appPlatform !== 'web' && !isTrafficLightVisible;
|
||||
const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible;
|
||||
const isInGroupView = !!searchParams?.get('group');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 flex h-11 w-full items-center py-2 pr-6',
|
||||
'titlebar z-10 flex h-11 w-full items-center py-2 pr-4 sm:pr-6',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
|
||||
)}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
|
||||
@@ -128,12 +128,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={windowButtonVisible}
|
||||
showMaximize={windowButtonVisible}
|
||||
showClose={windowButtonVisible}
|
||||
/>
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={windowButtonVisible}
|
||||
showMaximize={windowButtonVisible}
|
||||
showClose={windowButtonVisible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
|
||||
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
setAboutDialogVisible(true);
|
||||
@@ -77,6 +78,13 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsAutoCheckUpdates(settings.autoCheckUpdates);
|
||||
};
|
||||
|
||||
const toggleScreenWakeLock = () => {
|
||||
settings.screenWakeLock = !settings.screenWakeLock;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsScreenWakeLock(settings.screenWakeLock);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
@@ -146,6 +154,11 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
onClick={toggleAutoCheckUpdates}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Keep Screen Awake')}
|
||||
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
|
||||
<hr className='border-base-200 my-1' />
|
||||
|
||||
@@ -33,6 +33,7 @@ import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
import Bookshelf from './components/Bookshelf';
|
||||
import BookDetailModal from '@/components/BookDetailModal';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
|
||||
const LibraryPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -65,6 +66,7 @@ const LibraryPage = () => {
|
||||
});
|
||||
|
||||
usePullToRefresh(containerRef, pullLibrary);
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-200');
|
||||
@@ -384,7 +386,7 @@ const LibraryPage = () => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'mt-12 flex-grow overflow-auto px-2',
|
||||
'mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { BookDoc } from '@/libs/document';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useFoliateEvents } from '../hooks/useFoliateEvents';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { getFootnoteStyles, getStyles } from '@/utils/style';
|
||||
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { FootnoteHandler } from 'foliate-js/footnotes.js';
|
||||
import Popup from '@/components/Popup';
|
||||
@@ -48,7 +49,9 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const backgroundColor = getComputedStyle(popupContainer).backgroundColor;
|
||||
popupTheme.bg = backgroundColor;
|
||||
}
|
||||
renderer.setStyles?.(getStyles(viewSettings, popupTheme));
|
||||
const mainStyles = getStyles(viewSettings, popupTheme);
|
||||
const footnoteStyles = getFootnoteStyles();
|
||||
renderer.setStyles?.(`${mainStyles}\n${footnoteStyles}`);
|
||||
};
|
||||
|
||||
const handleRender = (e: Event) => {
|
||||
@@ -104,10 +107,43 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
setShowPopup(false);
|
||||
};
|
||||
|
||||
const handleFootnotePopupEvent = (event: CustomEvent) => {
|
||||
const { element, footnote } = event.detail;
|
||||
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
|
||||
if (!gridFrame) return;
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const triangPos = getPosition(element, rect, viewSettings.vertical);
|
||||
const popupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
viewSettings.vertical ? popupHeight : popupWidth,
|
||||
viewSettings.vertical ? popupWidth : popupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
if (footnoteRef.current) {
|
||||
const elem = document.createElement('p');
|
||||
elem.textContent = footnote;
|
||||
elem.setAttribute('style', `padding: 16px; hanging-punctuation: allow-end last;`);
|
||||
footnoteRef.current.replaceChildren(elem);
|
||||
setShowPopup(true);
|
||||
setTrianglePosition(triangPos);
|
||||
setPopupPosition(popupPos);
|
||||
}
|
||||
};
|
||||
|
||||
useFoliateEvents(view, {
|
||||
onLinkClick: docLinkHandler,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('footnote-popup', handleFootnotePopupEvent);
|
||||
return () => {
|
||||
eventDispatcher.off('footnote-popup', handleFootnotePopupEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (footnoteViewRef.current) {
|
||||
footnoteRef.current?.replaceChildren(footnoteViewRef.current);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import ReaderContent from './ReaderContent';
|
||||
@@ -19,6 +20,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
const { updateAppTheme } = useTheme();
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-100');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
|
||||
interface NoteEditorProps {
|
||||
onSave: (selection: TextSelection, note: string) => void;
|
||||
@@ -12,7 +13,7 @@ interface NoteEditorProps {
|
||||
|
||||
const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
const _ = useTranslation();
|
||||
const { notebookNewAnnotation, notebookEditAnnotation } = useNotebookStore();
|
||||
const { notebookNewAnnotation, notebookEditAnnotation, setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [note, setNote] = React.useState('');
|
||||
|
||||
@@ -49,6 +50,21 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
}
|
||||
};
|
||||
|
||||
useShortcuts({
|
||||
onSaveNote: () => {
|
||||
if (editorRef.current && editorRef.current.value) {
|
||||
handleSaveNote();
|
||||
}
|
||||
},
|
||||
onCloseNote: () => {
|
||||
if (notebookNewAnnotation) {
|
||||
setNotebookNewAnnotation(null)
|
||||
} else if (notebookEditAnnotation) {
|
||||
setNotebookEditAnnotation(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='note-editor-container bg-base-100 mt-2 rounded-md p-2'>
|
||||
<div className='flex w-full justify-between space-x-2'>
|
||||
|
||||
@@ -150,7 +150,9 @@ const Notebook: React.FC = ({}) => {
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
|
||||
'notebook-container bg-base-200 right-0 z-20 min-w-60 select-none',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isNotebookPinned && 'shadow-2xl',
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useEffect } from 'react';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { tauriGetWindowLogicalPosition } from '@/utils/window';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
export const useClickEvent = (
|
||||
bookKey: string,
|
||||
@@ -10,29 +13,47 @@ export const useClickEvent = (
|
||||
) => {
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const handleTurnPage = (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const handleTurnPage = async (
|
||||
msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
if (msg instanceof MessageEvent) {
|
||||
if (msg.data && msg.data.bookKey === bookKey) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
if (msg.data.type === 'iframe-single-click') {
|
||||
const viewElement = containerRef.current;
|
||||
if (viewElement) {
|
||||
const rect = viewElement.getBoundingClientRect();
|
||||
const { screenX, screenY } = msg.data;
|
||||
const consumed = eventDispatcher.dispatchSync('iframe-single-click', {
|
||||
screenX,
|
||||
screenY,
|
||||
});
|
||||
const { screenX } = msg.data;
|
||||
const viewRect = viewElement.getBoundingClientRect();
|
||||
let windowStartX;
|
||||
// Currently for tauri APP the window.screenX is always 0
|
||||
if (isTauriAppPlatform()) {
|
||||
if (['android', 'ios'].includes(getOSPlatform())) {
|
||||
windowStartX = 0;
|
||||
} else {
|
||||
const windowPosition = await tauriGetWindowLogicalPosition();
|
||||
windowStartX = windowPosition.x;
|
||||
}
|
||||
} else {
|
||||
windowStartX = window.screenX;
|
||||
}
|
||||
const viewStartX = windowStartX + viewRect.left;
|
||||
const viewCenterX = viewStartX + viewRect.width / 2;
|
||||
const consumed = eventDispatcher.dispatchSync('iframe-single-click');
|
||||
if (!consumed) {
|
||||
const centerStartX = rect.left + rect.width * 0.375;
|
||||
const centerEndX = rect.left + rect.width * 0.625;
|
||||
const centerStartX = viewStartX + viewRect.width * 0.375;
|
||||
const centerEndX = viewStartX + viewRect.width * 0.625;
|
||||
if (screenX >= centerStartX && screenX <= centerEndX) {
|
||||
// toggle visibility of the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? null : bookKey);
|
||||
} else if (!viewSettings.disableClick! && screenX >= rect.left + rect.width / 2) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (!viewSettings.disableClick! && screenX < rect.left + rect.width / 2) {
|
||||
viewRef.current?.goLeft();
|
||||
} else {
|
||||
if (hoveredBookKey) {
|
||||
setHoveredBookKey(null);
|
||||
}
|
||||
if (!viewSettings.disableClick! && screenX >= viewCenterX) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (!viewSettings.disableClick! && screenX < viewCenterX) {
|
||||
viewRef.current?.goLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +132,13 @@ export const useTouchEvent = (
|
||||
if (touch) {
|
||||
touchEnd = touch;
|
||||
}
|
||||
if (hoveredBookKey && touchEnd) {
|
||||
const deltaY = touchEnd.screenY - touchStart.screenY;
|
||||
const deltaX = touchEnd.screenX - touchStart.screenX;
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
||||
setHoveredBookKey(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: IframeTouchEvent) => {
|
||||
@@ -133,6 +161,10 @@ export const useTouchEvent = (
|
||||
) {
|
||||
// swipe up to toggle the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? null : bookKey);
|
||||
} else {
|
||||
if (hoveredBookKey) {
|
||||
setHoveredBookKey(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DOUBLE_CLICK_INTERVAL_THRESHOLD_MS,
|
||||
LONG_HOLD_THRESHOLD,
|
||||
} from '@/services/constants';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
const doubleClickEnabled =
|
||||
@@ -121,6 +122,14 @@ export const handleClick = (bookKey: string, event: MouseEvent) => {
|
||||
if (['sup', 'a', 'audio', 'video'].includes(element.tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
if (element.classList.contains('js_readerFooterNote')) {
|
||||
eventDispatcher.dispatch('footnote-popup', {
|
||||
bookKey,
|
||||
element,
|
||||
footnote: element.getAttribute('data-wr-footernote') || '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,13 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
||||
const fetchBookDetails = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const details = await appService.fetchBookDetails(book, settings);
|
||||
setBookMeta(details);
|
||||
setLoading(false);
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
try {
|
||||
const details = await appService.fetchBookDetails(book, settings);
|
||||
setBookMeta(details);
|
||||
} finally {
|
||||
if (loadingTimeout) clearTimeout(loadingTimeout);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchBookDetails();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -193,14 +196,21 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
</div>
|
||||
</Dialog>
|
||||
{showDeleteAlert && (
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
onCancel={() => {
|
||||
setShowDeleteAlert(false);
|
||||
}}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'fixed bottom-0 left-0 right-0 z-50 flex justify-center',
|
||||
'pb-[calc(env(safe-area-inset-bottom)+16px)]',
|
||||
)}
|
||||
>
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
onCancel={() => {
|
||||
setShowDeleteAlert(false);
|
||||
}}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ const Popup = ({
|
||||
}) => (
|
||||
<div>
|
||||
<div
|
||||
className={`triangle text-base-200 absolute z-40 ${triangleClassName}`}
|
||||
className={`triangle text-base-300 absolute z-40 ${triangleClassName}`}
|
||||
style={{
|
||||
left:
|
||||
trianglePosition?.dir === 'left'
|
||||
@@ -67,7 +67,7 @@ const Popup = ({
|
||||
/>
|
||||
<div
|
||||
id='popup-container'
|
||||
className={`bg-base-200 absolute z-30 rounded-lg font-sans shadow-xl ${className}`}
|
||||
className={`bg-base-300 absolute z-30 rounded-lg font-sans shadow-xl ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
|
||||
@@ -64,7 +64,8 @@ export const Toast = () => {
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'max-h-[50vh] min-w-[30vw] overflow-scroll whitespace-normal break-words text-center',
|
||||
'max-h-[50vh] min-w-[30vw] max-w-80',
|
||||
'overflow-scroll whitespace-normal break-words text-center',
|
||||
messageClass.current,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface ShortcutConfig {
|
||||
onZoomIn: string[];
|
||||
onZoomOut: string[];
|
||||
onResetZoom: string[];
|
||||
onSaveNote: string[];
|
||||
onCloseNote: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
@@ -36,6 +38,8 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onZoomIn: ['ctrl+=', 'cmd+=', 'shift+='],
|
||||
onZoomOut: ['ctrl+-', 'cmd+-', 'shift+-'],
|
||||
onResetZoom: ['ctrl+0', 'cmd+0'],
|
||||
onSaveNote: ['ctrl+Enter'],
|
||||
onCloseNote: ['Escape']
|
||||
};
|
||||
|
||||
// Load shortcuts from localStorage or fallback to defaults
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseLongPressOptions {
|
||||
onLongPress?: () => void;
|
||||
onTap?: () => void;
|
||||
onLongPress?: () => void;
|
||||
onContextMenu?: () => void;
|
||||
onCancel?: () => void;
|
||||
threshold?: number;
|
||||
moveThreshold?: number;
|
||||
@@ -21,8 +22,9 @@ interface UseLongPressResult {
|
||||
}
|
||||
|
||||
export const useLongPress = ({
|
||||
onLongPress,
|
||||
onTap,
|
||||
onLongPress,
|
||||
onContextMenu,
|
||||
onCancel,
|
||||
threshold = 500,
|
||||
moveThreshold = 10,
|
||||
@@ -105,11 +107,16 @@ export const useLongPress = ({
|
||||
[onCancel, reset],
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
if (isLongPressTriggered.current) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (onContextMenu) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onContextMenu();
|
||||
}
|
||||
},
|
||||
[onContextMenu],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useScreenWakeLock = (lock: boolean) => {
|
||||
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const requestWakeLock = async () => {
|
||||
try {
|
||||
if ('wakeLock' in navigator) {
|
||||
wakeLockRef.current = await navigator.wakeLock.request('screen');
|
||||
|
||||
wakeLockRef.current.addEventListener('release', () => {
|
||||
wakeLockRef.current = null;
|
||||
});
|
||||
|
||||
console.log('Wake lock acquired');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to acquire wake lock:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (lock) {
|
||||
requestWakeLock();
|
||||
} else if (wakeLockRef.current) {
|
||||
wakeLockRef.current.release();
|
||||
wakeLockRef.current = null;
|
||||
console.log('Wake lock released');
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (wakeLockRef.current) {
|
||||
wakeLockRef.current.release();
|
||||
wakeLockRef.current = null;
|
||||
console.log('Wake lock released');
|
||||
}
|
||||
};
|
||||
}, [lock]);
|
||||
};
|
||||
@@ -63,8 +63,8 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
handler &&
|
||||
shortcutList?.some((shortcut) =>
|
||||
isShortcutMatch(shortcut, key, ctrlKey, altKey, metaKey, shiftKey),
|
||||
)
|
||||
) {
|
||||
)
|
||||
) {
|
||||
handler();
|
||||
return true;
|
||||
}
|
||||
@@ -76,16 +76,23 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
// Check if the focus is on an input, textarea, or contenteditable element
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
const isInteractiveElement =
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.isContentEditable;
|
||||
activeElement.isContentEditable);
|
||||
|
||||
if (isInteractiveElement) {
|
||||
const isNoteEditor = (activeElement.tagName === 'TEXTAREA' && activeElement.classList.contains('note-editor'))
|
||||
|
||||
if (isInteractiveElement && !isNoteEditor) {
|
||||
return; // Skip handling if the user is typing in an input, textarea, or contenteditable
|
||||
}
|
||||
|
||||
if (event instanceof KeyboardEvent) {
|
||||
const { key, ctrlKey, altKey, metaKey, shiftKey } = event;
|
||||
|
||||
if (isNoteEditor && !((key === "Enter" && ctrlKey) || (key == "Escape"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handled = processKeyEvent(key.toLowerCase(), ctrlKey, altKey, metaKey, shiftKey);
|
||||
if (handled) event.preventDefault();
|
||||
} else if (
|
||||
|
||||
@@ -36,6 +36,7 @@ import { getOSPlatform, isCJKEnv, isValidURL } from '@/utils/misc';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
|
||||
import { ProgressHandler } from '@/utils/transfer';
|
||||
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
osPlatform: string = getOSPlatform();
|
||||
@@ -338,7 +339,7 @@ export abstract class BaseAppService implements AppService {
|
||||
} else if (book.url) {
|
||||
file = await new RemoteFile(book.url).open();
|
||||
} else {
|
||||
throw new Error('Book file not found');
|
||||
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
|
||||
}
|
||||
return { book, file, config: await this.loadBookConfig(book, settings) };
|
||||
}
|
||||
@@ -357,6 +358,10 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async fetchBookDetails(book: Book, settings: SystemSettings) {
|
||||
const fp = getFilename(book);
|
||||
if (!(await this.fs.exists(fp, 'Books')) && book.uploadedAt) {
|
||||
await this.downloadBook(book);
|
||||
}
|
||||
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
|
||||
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
|
||||
return bookDoc.metadata;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
keepLogin: false,
|
||||
autoUpload: true,
|
||||
autoCheckUpdates: true,
|
||||
screenWakeLock: true,
|
||||
|
||||
lastSyncedAtBooks: 0,
|
||||
lastSyncedAtConfigs: 0,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const BOOK_FILE_NOT_FOUND_ERROR = 'Book file not found';
|
||||
@@ -21,6 +21,7 @@ export interface SystemSettings {
|
||||
keepLogin: boolean;
|
||||
autoUpload: boolean;
|
||||
autoCheckUpdates: boolean;
|
||||
screenWakeLock: boolean;
|
||||
|
||||
lastSyncedAtBooks: number;
|
||||
lastSyncedAtConfigs: number;
|
||||
|
||||
@@ -203,6 +203,22 @@ const getLayoutStyles = (
|
||||
}
|
||||
`;
|
||||
|
||||
export const getFootnoteStyles = () => `
|
||||
.duokan-footnote-content,
|
||||
.duokan-footnote-item {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p, li, blockquote, dd {
|
||||
text-indent: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ThemeCode {
|
||||
bg: string;
|
||||
fg: string;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export const tauriGetWindowLogicalPosition = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
const factor = await currentWindow.scaleFactor();
|
||||
const physicalPos = await currentWindow.outerPosition();
|
||||
return { x: physicalPos.x / factor, y: physicalPos.y / factor };
|
||||
};
|
||||
|
||||
export const tauriHandleMinimize = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
getCurrentWindow().minimize();
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: aa61cb4ae0...601d70dbe4
Reference in New Issue
Block a user