forked from akai/readest
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5163d01e08 | |||
| 2763c18d6f | |||
| 71d0735917 | |||
| 3484c893df | |||
| e885d22631 | |||
| 3ad26d9d8f | |||
| b699fc98c2 | |||
| 2c9fe8e4f4 |
@@ -12,6 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_id: ${{ steps.get-release.outputs.release_id }}
|
||||
release_tag: ${{ steps.get-release.outputs.release_tag }}
|
||||
release_note: ${{ steps.get-release-notes.outputs.release_note }}
|
||||
|
||||
steps:
|
||||
@@ -30,6 +31,7 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
core.setOutput('release_id', data.id);
|
||||
core.setOutput('release_tag', data.tag_name);
|
||||
- name: get release notes
|
||||
id: get-release-notes
|
||||
uses: actions/github-script@v7
|
||||
@@ -138,6 +140,26 @@ jobs:
|
||||
releaseBody: ${{ needs.get-release.outputs.release_note }}
|
||||
args: ${{ matrix.config.args || '' }}
|
||||
|
||||
- name: upload portable binaries (Windows only)
|
||||
if: matrix.config.os == 'windows-latest'
|
||||
run: |
|
||||
echo "Uploading Portable Binaries"
|
||||
arch=${{ matrix.config.arch }}
|
||||
version=$PACKAGE_VERSION
|
||||
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
zip_name="Readest_${version}_x64-portable.zip"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
zip_name="Readest_${version}_arm64-portable.zip"
|
||||
else
|
||||
echo "Unknown architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
zip -j $zip_name apps/readest-app/src-tauri/target/release/readest.exe
|
||||
echo "Uploading $zip_name to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $zip_name --clobber
|
||||
|
||||
update-release:
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -153,11 +175,12 @@ jobs:
|
||||
release_note: ${{ needs.get-release.outputs.release_note }}
|
||||
with:
|
||||
script: |
|
||||
const body = `## Release Highlight\n${process.env.release_note}`;
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.release_id,
|
||||
body: process.env.release_note,
|
||||
body: body,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.8.9",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@supabase/supabase-js": "^2.47.7",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-http": "^2.2.0",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Über Readest",
|
||||
"Add your notes here...": "Fügen Sie hier Ihre Notizen hinzu...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
|
||||
"Auto Mode": "Automatischer Modus",
|
||||
"Behavior": "Verhalten",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Grasgrün",
|
||||
"Gray": "Grau",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Horizontale Richtung",
|
||||
"Hyphenation": "Silbentrennung",
|
||||
"Identifier:": "Kennung:",
|
||||
"Import Books": "Bücher importieren",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Aktualisiert:",
|
||||
"User avatar": "Benutzer-Avatar",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Vertikale Richtung",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
|
||||
"Writing Mode": "Schreibmodus",
|
||||
"Your Library": "Ihre Bibliothek"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Acerca de Readest",
|
||||
"Add your notes here...": "Añade tus notas aquí...",
|
||||
"Animation": "Animación",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
|
||||
"Auto Mode": "Modo automático",
|
||||
"Behavior": "Comportamiento",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Hierba",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Dirección horizontal",
|
||||
"Hyphenation": "Guionización",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar libros",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Actualizado:",
|
||||
"User avatar": "Avatar del usuario",
|
||||
"Version {{version}}": "Versión {{version}}",
|
||||
"Vertical Direction": "Dirección vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
|
||||
"Writing Mode": "Modo de escritura",
|
||||
"Your Library": "Tu biblioteca"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "À propos de Readest",
|
||||
"Add your notes here...": "Ajoutez vos notes ici...",
|
||||
"Animation": "Animation",
|
||||
"Apply": "Appliquer",
|
||||
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
|
||||
"Auto Mode": "Mode automatique",
|
||||
"Behavior": "Comportement",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Herbe",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direction horizontale",
|
||||
"Hyphenation": "Césure",
|
||||
"Identifier:": "Identifiant :",
|
||||
"Import Books": "Importer des livres",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Mis à jour :",
|
||||
"User avatar": "Avatar de l'utilisateur",
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Direction verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
|
||||
"Writing Mode": "Mode écriture",
|
||||
"Your Library": "Votre bibliothèque"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Tentang Readest",
|
||||
"Add your notes here...": "Tambahkan catatan Anda di sini...",
|
||||
"Animation": "Animasi",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
"Behavior": "Perilaku",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Rumput",
|
||||
"Gray": "Abu-abu",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Arah Horizontal",
|
||||
"Hyphenation": "Pemenggalan Kata",
|
||||
"Identifier:": "Pengenal:",
|
||||
"Import Books": "Impor Buku",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Diperbarui:",
|
||||
"User avatar": "Avatar Pengguna",
|
||||
"Version {{version}}": "Versi {{version}}",
|
||||
"Vertical Direction": "Arah Vertikal",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Informazioni su Readest",
|
||||
"Add your notes here...": "Aggiungi qui le tue note...",
|
||||
"Animation": "Animazione",
|
||||
"Apply": "Applica",
|
||||
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
|
||||
"Auto Mode": "Modalità automatica",
|
||||
"Behavior": "Comportamento",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Erba",
|
||||
"Gray": "Grigio",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direzione orizzontale",
|
||||
"Hyphenation": "Sillabazione",
|
||||
"Identifier:": "Identificatore:",
|
||||
"Import Books": "Importa libri",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Aggiornato:",
|
||||
"User avatar": "Avatar utente",
|
||||
"Version {{version}}": "Versione {{version}}",
|
||||
"Vertical Direction": "Direzione verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
|
||||
"Writing Mode": "Modalità scrittura",
|
||||
"Your Library": "La tua biblioteca"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readestについて",
|
||||
"Add your notes here...": "ここにメモを追加...",
|
||||
"Animation": "アニメーション",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
"Behavior": "動作",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "グリーン",
|
||||
"Gray": "グレー",
|
||||
"Gruvbox": "グルーブボックス",
|
||||
"Horizontal Direction": "横書",
|
||||
"Hyphenation": "ハイフネーション",
|
||||
"Identifier:": "識別子:",
|
||||
"Import Books": "書籍をインポート",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "更新日:",
|
||||
"User avatar": "ユーザーアバター",
|
||||
"Version {{version}}": "バージョン {{version}}",
|
||||
"Vertical Direction": "縦書",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readest 정보",
|
||||
"Add your notes here...": "여기에 메모를 추가하세요...",
|
||||
"Animation": "애니메이션",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
"Behavior": "동작",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "잔디색",
|
||||
"Gray": "회색",
|
||||
"Gruvbox": "그러브박스",
|
||||
"Horizontal Direction": "수평 방향",
|
||||
"Hyphenation": "하이픈 넣기",
|
||||
"Identifier:": "식별자:",
|
||||
"Import Books": "책 가져오기",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "업데이트일:",
|
||||
"User avatar": "사용자 아바타",
|
||||
"Version {{version}}": "버전 {{version}}",
|
||||
"Vertical Direction": "수직 방향",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Sobre o Readest",
|
||||
"Add your notes here...": "Adicione suas notas aqui...",
|
||||
"Animation": "Animação",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
|
||||
"Auto Mode": "Modo Automático",
|
||||
"Behavior": "Comportamento",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Grama",
|
||||
"Gray": "Cinza",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Direção Horizontal",
|
||||
"Hyphenation": "Hifenização",
|
||||
"Identifier:": "Identificador:",
|
||||
"Import Books": "Importar Livros",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Atualizado:",
|
||||
"User avatar": "Avatar do usuário",
|
||||
"Version {{version}}": "Versão {{version}}",
|
||||
"Vertical Direction": "Direção Vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
|
||||
"Writing Mode": "Modo de Escrita",
|
||||
"Your Library": "Sua Biblioteca"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "О Readest",
|
||||
"Add your notes here...": "Добавьте свои заметки здесь...",
|
||||
"Animation": "Анимация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
"Behavior": "Поведение",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Трава",
|
||||
"Gray": "Серый",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Горизонтальное направление",
|
||||
"Hyphenation": "Перенос слов",
|
||||
"Identifier:": "Идентификатор:",
|
||||
"Import Books": "Импорт книг",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Обновлено:",
|
||||
"User avatar": "Аватар пользователя",
|
||||
"Version {{version}}": "Версия {{version}}",
|
||||
"Vertical Direction": "Вертикальное направление",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
|
||||
"Writing Mode": "Режим записи",
|
||||
"Your Library": "Ваша библиотека"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Readest Hakkında",
|
||||
"Add your notes here...": "Notlarınızı buraya ekleyin...",
|
||||
"Animation": "Animasyon",
|
||||
"Apply": "Uygula",
|
||||
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
|
||||
"Auto Mode": "Otomatik Mod",
|
||||
"Behavior": "Davranış",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Çimen",
|
||||
"Gray": "Gri",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Yatay Yön",
|
||||
"Hyphenation": "Heceleme",
|
||||
"Identifier:": "Tanımlayıcı:",
|
||||
"Import Books": "Kitapları İçe Aktar",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Güncellendi:",
|
||||
"User avatar": "Kullanıcı avatarı",
|
||||
"Version {{version}}": "Sürüm {{version}}",
|
||||
"Vertical Direction": "Dikey Yön",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
|
||||
"Writing Mode": "Yazma Modu",
|
||||
"Your Library": "Kütüphaneniz"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "Về Readest",
|
||||
"Add your notes here...": "Thêm ghi chú của bạn vào đây...",
|
||||
"Animation": "Hiệu ứng động",
|
||||
"Apply": "Áp dụng",
|
||||
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
"Behavior": "Hành vi",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "Xanh cỏ",
|
||||
"Gray": "Xám",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Horizontal Direction": "Hướng ngang",
|
||||
"Hyphenation": "Gạch nối từ",
|
||||
"Identifier:": "Định danh:",
|
||||
"Import Books": "Nhập sách",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "Cập nhật:",
|
||||
"User avatar": "Ảnh đại diện",
|
||||
"Version {{version}}": "Phiên bản {{version}}",
|
||||
"Vertical Direction": "Hướng dọc",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "关于 Readest",
|
||||
"Add your notes here...": "在这里添加您的笔记...",
|
||||
"Animation": "动画",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
"Behavior": "行为",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Horizontal Direction": "横排",
|
||||
"Hyphenation": "断字",
|
||||
"Identifier:": "识别码",
|
||||
"Import Books": "导入书籍",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "更新日期",
|
||||
"User avatar": "用户头像",
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "竖排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "书库空空如也,您可以导入您的书籍并随时阅读。",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "书库"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"About Readest": "關於 Readest",
|
||||
"Add your notes here...": "在這裡添加您的筆記...",
|
||||
"Animation": "動畫",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
"Behavior": "行為",
|
||||
@@ -42,6 +43,7 @@
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Horizontal Direction": "橫排",
|
||||
"Hyphenation": "斷字",
|
||||
"Identifier:": "識別碼",
|
||||
"Import Books": "導入書籍",
|
||||
@@ -107,6 +109,8 @@
|
||||
"Updated:": "更新日期",
|
||||
"User avatar": "用戶頭像",
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "豎排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "書庫"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.0": {
|
||||
"date": "2025-01-04",
|
||||
"notes": [
|
||||
"Better Custom CSS editor and OAuth process.",
|
||||
"Add vertical/horizontal switch for CJK books.",
|
||||
"Fix dropdown close overlay not working on Windows/Linux."
|
||||
]
|
||||
},
|
||||
"0.8.9": {
|
||||
"date": "2025-01-02",
|
||||
"notes": [
|
||||
|
||||
Generated
+137
-3
@@ -15,6 +15,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tauri-plugin-cli",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-devtools",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
@@ -25,6 +26,7 @@ dependencies = [
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
]
|
||||
|
||||
@@ -891,6 +893,26 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@@ -1033,6 +1055,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -1277,6 +1305,15 @@ dependencies = [
|
||||
"syn 2.0.91",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||
dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.10"
|
||||
@@ -2016,6 +2053,12 @@ dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
@@ -3224,6 +3267,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -4004,7 +4057,7 @@ dependencies = [
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
"windows-registry 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4084,6 +4137,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
"trim-in-place",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.36.0"
|
||||
@@ -4958,6 +5022,26 @@ dependencies = [
|
||||
"thiserror 2.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35d51ffd286073414d26353bcfc9e83e3cd63f96fa7f7a912f92f2118e5de5a6"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.3.0",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-devtools"
|
||||
version = "2.0.0"
|
||||
@@ -5156,6 +5240,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f36019ee9832dc99e4450bb55a21cfad8633b19c2c18bd17c7741939b070ede"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror 2.0.9",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.3.0"
|
||||
@@ -5422,6 +5521,15 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
@@ -5783,6 +5891,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trim-in-place"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -6332,7 +6446,7 @@ dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -6365,7 +6479,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-strings 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -6388,6 +6513,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ authors = ["Bilingify LLC"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.71"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -34,6 +34,7 @@ tauri-plugin-shell = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
[patch.crates-io]
|
||||
tauri = { path = "../../../packages/tauri/crates/tauri" }
|
||||
|
||||
@@ -44,4 +45,5 @@ rand = "0.8"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
tauri-plugin-single-instance = "2.2.0"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"process:allow-restart",
|
||||
"oauth:allow-start",
|
||||
"oauth:allow-cancel",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@ async fn start_server(window: Window) -> Result<u16, String> {
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
args: Vec<String>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default()
|
||||
@@ -83,6 +89,17 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
app.emit("single-instance", Payload { args: argv, cwd }).unwrap();
|
||||
}));
|
||||
|
||||
let builder = builder.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
@@ -135,6 +152,12 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
|
||||
@@ -130,6 +130,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"deep-link": {
|
||||
"mobile": [{ "host": "web.readest.com" }],
|
||||
"desktop": {
|
||||
"schemes": ["readest"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0OTAxMURGQkUzQjFENTQKUldSVUhUdSszeEdRNUExdmFkWnlvYWNYNG5wamkxMmUxRk5SejlMOTJVd28yNXlVTDh6Wld4OC8K",
|
||||
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
|
||||
|
||||
@@ -17,12 +17,18 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
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 { handleAuthCallback } from '@/helpers/auth';
|
||||
|
||||
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
|
||||
|
||||
interface SingleInstancePayload {
|
||||
args: string[];
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
interface ProviderLoginProp {
|
||||
provider: OAuthProvider;
|
||||
handleSignIn: (provider: OAuthProvider) => void;
|
||||
@@ -64,7 +70,10 @@ export default function AuthPage() {
|
||||
provider,
|
||||
options: {
|
||||
skipBrowserRedirect: true,
|
||||
redirectTo: `http://localhost:${port}`,
|
||||
redirectTo:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'readest://auth/callback'
|
||||
: `http://localhost:${port}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,34 +84,54 @@ export default function AuthPage() {
|
||||
openUrl(data.url);
|
||||
};
|
||||
|
||||
const startOAuthServer = async () => {
|
||||
const handleOAuthUrl = async (url: string) => {
|
||||
console.log('Received OAuth URL:', url);
|
||||
const hashMatch = url.match(/#(.*)/);
|
||||
if (hashMatch) {
|
||||
const hash = hashMatch[1];
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const next = params.get('next') ?? '/';
|
||||
if (accessToken) {
|
||||
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startTauriOAuth = async () => {
|
||||
try {
|
||||
const port = await start();
|
||||
setPort(port);
|
||||
console.log(`OAuth server started on port ${port}`);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
currentWindow.listen('single-instance', ({ event, payload }) => {
|
||||
console.log('Received deep link:', event, payload);
|
||||
const { args } = payload as SingleInstancePayload;
|
||||
if (args?.[1]) {
|
||||
handleOAuthUrl(args[1]);
|
||||
}
|
||||
});
|
||||
await onOpenUrl((urls) => {
|
||||
urls.forEach((url) => {
|
||||
handleOAuthUrl(url);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const port = await start();
|
||||
setPort(port);
|
||||
console.log(`OAuth server started on port ${port}`);
|
||||
|
||||
await onUrl((url) => {
|
||||
console.log('Received OAuth URL:', url);
|
||||
const hashMatch = url.match(/#(.*)/);
|
||||
if (hashMatch) {
|
||||
const hash = hashMatch[1];
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get('access_token');
|
||||
const refreshToken = params.get('refresh_token');
|
||||
const next = params.get('next') ?? '/';
|
||||
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
|
||||
}
|
||||
});
|
||||
|
||||
await onInvalidUrl((url) => {
|
||||
console.log('Received invalid OAuth URL:', url);
|
||||
});
|
||||
await onUrl(handleOAuthUrl);
|
||||
await onInvalidUrl((url) => {
|
||||
console.log('Received invalid OAuth URL:', url);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting OAuth server:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const stopOAuthServer = async () => {
|
||||
const stopTauriOAuth = async () => {
|
||||
try {
|
||||
if (port) {
|
||||
await cancel(port);
|
||||
@@ -126,10 +155,10 @@ export default function AuthPage() {
|
||||
if (isOAuthServerRunning.current) return;
|
||||
isOAuthServerRunning.current = true;
|
||||
|
||||
startOAuthServer();
|
||||
startTauriOAuth();
|
||||
return () => {
|
||||
isOAuthServerRunning.current = false;
|
||||
stopOAuthServer();
|
||||
stopTauriOAuth();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -156,6 +185,9 @@ export default function AuthPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For tauri app development, use a custom OAuth server to handle the OAuth callback
|
||||
// For tauri app production, use deeplink to handle the OAuth callback
|
||||
// For web app, use the built-in OAuth callback page /auth/callback
|
||||
return isTauriAppPlatform() ? (
|
||||
<div className='flex pt-11'>
|
||||
<button
|
||||
|
||||
@@ -69,9 +69,14 @@ const LibraryPage = () => {
|
||||
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
|
||||
const bookIds: string[] = [];
|
||||
for (const file of openWithFiles) {
|
||||
const book = await appService.importBook(file, libraryBooks);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
console.log('Open with book:', file);
|
||||
try {
|
||||
const book = await appService.importBook(file, libraryBooks);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to import book:', file, error);
|
||||
}
|
||||
}
|
||||
setLibrary(libraryBooks);
|
||||
|
||||
@@ -28,6 +28,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const { themeCode } = useTheme();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const footnoteHandler = new FootnoteHandler();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,7 +74,13 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
const rect = gridFrame.getBoundingClientRect();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const triangPos = getPosition(detail.a, rect, viewSettings.vertical);
|
||||
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
|
||||
const popupPos = getPopupPosition(
|
||||
triangPos,
|
||||
rect,
|
||||
viewSettings.vertical ? popupHeight : popupWidth,
|
||||
viewSettings.vertical ? popupWidth : popupHeight,
|
||||
popupPadding,
|
||||
);
|
||||
setTrianglePosition(triangPos);
|
||||
setPopupPosition(popupPos);
|
||||
|
||||
@@ -107,6 +114,9 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
}
|
||||
}, [footnoteRef]);
|
||||
|
||||
const width = viewSettings.vertical ? popupHeight : popupWidth;
|
||||
const height = viewSettings.vertical ? popupWidth : popupHeight;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showPopup && (
|
||||
@@ -117,8 +127,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
/>
|
||||
)}
|
||||
<Popup
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
width={width}
|
||||
height={height}
|
||||
position={showPopup ? popupPosition! : undefined}
|
||||
trianglePosition={showPopup ? trianglePosition! : undefined}
|
||||
className='select-text overflow-y-auto'
|
||||
@@ -127,8 +137,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
|
||||
className=''
|
||||
ref={footnoteRef}
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
></div>
|
||||
</Popup>
|
||||
|
||||
@@ -88,6 +88,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
const { book } = getBookData(bookKey) || {};
|
||||
const { isPrimary } = getViewState(bookKey) || {};
|
||||
if (isPrimary && book && config) {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
await saveConfig(envConfig, bookKey, config, settings);
|
||||
}
|
||||
};
|
||||
@@ -110,6 +111,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
};
|
||||
|
||||
const handleCloseBooks = async () => {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
await Promise.all(bookKeys.map((key) => saveConfigAndCloseBook(key)));
|
||||
await saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MdOutlineAutoMode } from 'react-icons/md';
|
||||
import { MdOutlineTextRotationDown, MdOutlineTextRotationNone } from 'react-icons/md';
|
||||
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import NumberInput from './NumberInput';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { getBookLangCode } from '@/utils/book';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey)!;
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
@@ -23,6 +30,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [maxColumnCount, setMaxColumnCount] = useState(viewSettings.maxColumnCount!);
|
||||
const [maxInlineSize, setMaxInlineSize] = useState(viewSettings.maxInlineSize!);
|
||||
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
|
||||
const [writingMode, setWritingMode] = useState(viewSettings.writingMode!);
|
||||
|
||||
useEffect(() => {
|
||||
viewSettings.lineHeight = lineHeight;
|
||||
@@ -111,8 +119,55 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [maxInlineSize]);
|
||||
|
||||
useEffect(() => {
|
||||
// global settings are not supported for writing mode
|
||||
viewSettings.writingMode = writingMode;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [writingMode]);
|
||||
|
||||
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
|
||||
const isCJKBook = langCode === 'zh' || langCode === 'ja' || langCode === 'ko';
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
{isCJKBook && (
|
||||
<div className='w-full'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Writing Mode')}</h2>
|
||||
<div className='flex gap-2'>
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Default')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('auto')}
|
||||
>
|
||||
<MdOutlineAutoMode size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Horizontal Direction')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('horizontal-tb')}
|
||||
>
|
||||
<MdOutlineTextRotationNone size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip={_('Vertical Direction')}>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('vertical-rl')}
|
||||
>
|
||||
<MdOutlineTextRotationDown size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Paragraph')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const cssRegex =
|
||||
/((?:\s*)([\w#.@*,:\-.:>+~$$$$\"=(),*\s]+)\s*{(?:[\s]*)((?:[^\}]+[:][^\}]+;?)*)*\s*}(?:\s*))/gim;
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import cssValidate from '@/utils/css';
|
||||
|
||||
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -18,44 +17,51 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const [animated, setAnimated] = useState(viewSettings.animated!);
|
||||
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick!);
|
||||
const [userStylesheet, setUserStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
let cssInput = userStylesheet;
|
||||
|
||||
const validateCSS = (css: string) => {
|
||||
return cssRegex.test(css);
|
||||
};
|
||||
|
||||
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
cssInput = e.target.value;
|
||||
const cssInput = e.target.value;
|
||||
setDraftStylesheet(cssInput);
|
||||
setDraftStylesheetSaved(false);
|
||||
|
||||
try {
|
||||
const formattedCSS = cssbeautify(cssInput, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
setUserStylesheet(formattedCSS);
|
||||
|
||||
if (cssInput && !validateCSS(cssInput)) {
|
||||
throw new Error('Invalid CSS');
|
||||
const { isValid, error } = cssValidate(cssInput);
|
||||
if (cssInput && !isValid) {
|
||||
throw new Error(error || 'Invalid CSS');
|
||||
}
|
||||
setError(null);
|
||||
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
}
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
} catch (err) {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
console.log('CSS Error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const applyStyles = () => {
|
||||
const formattedCSS = cssbeautify(draftStylesheet, {
|
||||
indent: ' ',
|
||||
openbrace: 'end-of-line',
|
||||
autosemicolon: true,
|
||||
});
|
||||
|
||||
setDraftStylesheet(formattedCSS);
|
||||
setDraftStylesheetSaved(true);
|
||||
viewSettings.userStylesheet = formattedCSS;
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
|
||||
if (isFontLayoutSettingsGlobal) {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
}
|
||||
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
};
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
@@ -90,7 +96,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='config-item config-item-top config-item-bottom'>
|
||||
<span className='text-gray-700'>{_('Paging Animation')}</span>
|
||||
@@ -107,7 +113,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Behavior')}</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='config-item config-item-top config-item-bottom'>
|
||||
<span className='text-gray-700'>{_('Disable Click-to-Flip')}</span>
|
||||
@@ -124,22 +130,34 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Custom CSS')}</h2>
|
||||
<div className={`card bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}>
|
||||
<div className='divide-y'>
|
||||
<div className='css-text-area config-item-top config-item-bottom p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={cssInput}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
|
||||
>
|
||||
<div className='relative p-1'>
|
||||
<textarea
|
||||
className='textarea textarea-ghost h-48 w-full border-0 p-3 !outline-none'
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={draftStylesheet}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
onChange={handleUserStylesheetChange}
|
||||
/>
|
||||
<button
|
||||
className={clsx(
|
||||
'btn btn-ghost bg-base-200 absolute bottom-2 right-4 h-8 min-h-8 px-4 py-2',
|
||||
draftStylesheetSaved ? 'hidden' : '',
|
||||
error ? 'btn-disabled' : '',
|
||||
)}
|
||||
onClick={applyStyles}
|
||||
disabled={!!error}
|
||||
>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <p className='mt-1 text-sm text-red-500'>{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -105,9 +105,9 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='title-author flex h-40 flex-col justify-between pr-4'>
|
||||
<div className='title-author flex h-40 max-w-[60%] flex-col justify-between pr-4'>
|
||||
<div>
|
||||
<h2 className='text-base-content mb-2 line-clamp-2 text-2xl font-bold'>
|
||||
<h2 className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
|
||||
{book.title || _('Untitled')}
|
||||
</h2>
|
||||
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
|
||||
|
||||
@@ -34,7 +34,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
: children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='dropdown-container'>
|
||||
{isOpen && (
|
||||
<div className='fixed inset-0 bg-transparent' onClick={() => setIsDropdownOpen(false)} />
|
||||
)}
|
||||
@@ -48,7 +48,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
</div>
|
||||
{isOpen && childrenWithToggle}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const Popup = ({
|
||||
/>
|
||||
<div
|
||||
id='popup-container'
|
||||
className={`bg-base-200 absolute rounded-lg font-sans shadow-xl ${className}`}
|
||||
className={`bg-base-200 absolute z-30 rounded-lg font-sans shadow-xl ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
|
||||
@@ -53,6 +53,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
if (
|
||||
target.closest('.btn') ||
|
||||
target.closest('.window-button') ||
|
||||
target.closest('.dropdown-container') ||
|
||||
target.closest('.exclude-title-bar-mousedown')
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -29,10 +29,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
const syncSession = (session: { access_token: string; user: User } | null) => {
|
||||
console.log('Syncing session');
|
||||
if (session) {
|
||||
const { access_token, user } = session;
|
||||
setToken(access_token);
|
||||
@@ -46,10 +44,20 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
setUser(null);
|
||||
}
|
||||
};
|
||||
const fetchSession = async () => {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
syncSession(data.session);
|
||||
};
|
||||
|
||||
console.log('Fetching session');
|
||||
fetchSession();
|
||||
}, [token]);
|
||||
const { data: subscription } = supabase.auth.onAuthStateChange((_, session) => {
|
||||
syncSession(session);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = (newToken: string, newUser: User) => {
|
||||
console.log('Logging in');
|
||||
|
||||
@@ -28,7 +28,7 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onReloadPage: ['shift+r'],
|
||||
onQuitApp: ['ctrl+q', 'cmd+q'],
|
||||
onGoLeft: ['ArrowLeft', 'PageUp', 'h'],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l'],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l', ' '],
|
||||
onGoNext: ['ArrowDown', 'j'],
|
||||
onGoPrev: ['ArrowUp', 'k'],
|
||||
onGoBack: ['shift+ArrowLeft', 'shift+h'],
|
||||
|
||||
@@ -42,6 +42,7 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
maxInlineSize: 720,
|
||||
maxBlockSize: 1440,
|
||||
animated: false,
|
||||
writingMode: 'auto',
|
||||
vertical: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface BookLayout {
|
||||
maxInlineSize: number;
|
||||
maxBlockSize: number;
|
||||
animated: boolean;
|
||||
writingMode: string;
|
||||
vertical: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export const listFormater = (narrow = false, lang = userLang) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getBookLangCode = (lang: string | string[] | undefined) => {
|
||||
export const getBookLangCode = (lang: string | string[] | undefined) => {
|
||||
try {
|
||||
const bookLang = typeof lang === 'string' ? lang : lang?.[0];
|
||||
return bookLang ? bookLang.split('-')[0]! : 'en';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const cssValidate = (css: string) => {
|
||||
// Remove comments and normalize whitespace
|
||||
css = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
||||
|
||||
// CSS property pattern (validate both property name and value)
|
||||
const propertyPattern = /^[\s\n]*[-\w]+\s*:\s*[^;]+;?$/;
|
||||
|
||||
// Check if empty
|
||||
if (!css) return { isValid: false, error: 'Empty CSS' };
|
||||
|
||||
// Ensure balanced curly braces
|
||||
const openBraces = (css.match(/{/g) || []).length;
|
||||
const closeBraces = (css.match(/}/g) || []).length;
|
||||
if (openBraces !== closeBraces) {
|
||||
return { isValid: false, error: 'Unbalanced curly braces' };
|
||||
}
|
||||
|
||||
// Split into rule blocks
|
||||
const blocks = css
|
||||
.split('}')
|
||||
.map((block) => block.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const block of blocks) {
|
||||
// Ensure the block has a selector and declarations
|
||||
const parts = block.split('{').map((part) => part.trim());
|
||||
if (parts.length !== 2) {
|
||||
return { isValid: false, error: 'Invalid CSS structure' };
|
||||
}
|
||||
|
||||
const [selector, decls] = parts;
|
||||
|
||||
// Ensure selector is not empty
|
||||
if (!selector) {
|
||||
return { isValid: false, error: 'Missing selector' };
|
||||
}
|
||||
|
||||
// Ensure declarations are not empty
|
||||
if (!decls) {
|
||||
return { isValid: false, error: `Missing declarations for selector: ${selector}` };
|
||||
}
|
||||
|
||||
// Validate declarations
|
||||
const props = decls
|
||||
.split(';')
|
||||
.map((prop) => prop.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (props.length === 0) {
|
||||
return { isValid: false, error: `No valid properties for selector: ${selector}` };
|
||||
}
|
||||
|
||||
for (const prop of props) {
|
||||
// Check if property is missing a name or value
|
||||
if (!prop.includes(':')) {
|
||||
return { isValid: false, error: `Missing property or value: ${prop}` };
|
||||
}
|
||||
|
||||
const [name, value] = prop.split(':').map((part) => part.trim());
|
||||
if (!name) {
|
||||
return { isValid: false, error: `Missing property name: ${prop}` };
|
||||
}
|
||||
if (!value) {
|
||||
return { isValid: false, error: `Missing property value: ${prop}` };
|
||||
}
|
||||
|
||||
// Validate full property format
|
||||
if (!propertyPattern.test(prop.endsWith(';') ? prop : prop + ';')) {
|
||||
return { isValid: false, error: `Invalid property: ${prop}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, error: null };
|
||||
};
|
||||
|
||||
export default cssValidate;
|
||||
@@ -76,6 +76,7 @@ const getLayoutStyles = (
|
||||
justify: boolean,
|
||||
hyphenate: boolean,
|
||||
zoomLevel: number,
|
||||
writingMode: string,
|
||||
bg: string,
|
||||
fg: string,
|
||||
primary: string,
|
||||
@@ -122,6 +123,7 @@ const getLayoutStyles = (
|
||||
}
|
||||
html, body {
|
||||
color: ${fg};
|
||||
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode};`}
|
||||
text-align: var(--default-text-align);
|
||||
background-color: var(--theme-bg-color, transparent);
|
||||
background: var(--background-set, none);
|
||||
@@ -182,6 +184,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
|
||||
viewSettings.fullJustification!,
|
||||
viewSettings.hyphenation!,
|
||||
viewSettings.zoomLevel! / 100.0,
|
||||
viewSettings.writingMode!,
|
||||
themeCode.bg,
|
||||
themeCode.fg,
|
||||
themeCode.primary,
|
||||
|
||||
Generated
+10
@@ -50,6 +50,9 @@ importers:
|
||||
'@tauri-apps/plugin-cli':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@tauri-apps/plugin-deep-link':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@tauri-apps/plugin-dialog':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
@@ -787,6 +790,9 @@ packages:
|
||||
'@tauri-apps/plugin-cli@2.2.0':
|
||||
resolution: {integrity: sha512-rvNhMog9rHr01Xk+trBFKJ0eZICIvPkm9GX6ogB89/0hROU/lf+a/sb4vC0wtSeR7zrJuCSxwxYuvHCZheaYFA==}
|
||||
|
||||
'@tauri-apps/plugin-deep-link@2.2.0':
|
||||
resolution: {integrity: sha512-H6mkxr2KZ3XJcKL44tiq6cOjCw9DL8OgU1xjn3j26Qsn+H/roPFiyhR7CHuB8Ar+sQFj4YVlfmJwtBajK2FETQ==}
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.2.0':
|
||||
resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==}
|
||||
|
||||
@@ -3741,6 +3747,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-deep-link@2.2.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@tauri-apps/plugin-dialog@2.2.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
Reference in New Issue
Block a user