forked from akai/readest
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d4ad34e32 | |||
| 3239ff0a80 | |||
| 8b7e3c6f79 | |||
| ace4b78420 | |||
| 39ac89fead | |||
| 490fe7e70c | |||
| 06a7a8ea82 | |||
| b7f4a3503f | |||
| 835ed443a5 | |||
| 3f3c6983ba | |||
| f22ce1c225 | |||
| 9990de112f | |||
| ede37757db | |||
| 0c65d44bc9 | |||
| 07c9813ec1 |
@@ -56,27 +56,35 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
release: android
|
||||
rust_target: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
||||
- os: ubuntu-22.04
|
||||
release: linux
|
||||
arch: x86_64
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-22.04-arm
|
||||
release: linux
|
||||
arch: aarch64
|
||||
rust_target: aarch64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
release: macos
|
||||
arch: aarch64
|
||||
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
|
||||
args: '--target universal-apple-darwin'
|
||||
- os: windows-latest
|
||||
release: windows
|
||||
arch: x86_64
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
args: '--target x86_64-pc-windows-msvc'
|
||||
- os: windows-latest
|
||||
release: windows
|
||||
arch: aarch64
|
||||
rust_target: aarch64-pc-windows-msvc
|
||||
args: '--target aarch64-pc-windows-msvc --bundles nsis'
|
||||
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -94,6 +102,21 @@ jobs:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: setup Java (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: setup Android SDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: install NDK (for Android build only)
|
||||
if: matrix.config.release == 'android'
|
||||
run: sdkmanager "ndk;27.0.11902837"
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -109,6 +132,12 @@ jobs:
|
||||
with:
|
||||
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
|
||||
- name: create .env.local file for Next.js
|
||||
run: |
|
||||
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
|
||||
@@ -116,17 +145,43 @@ jobs:
|
||||
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
|
||||
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
|
||||
cp .env.local apps/readest-app/.env.local
|
||||
|
||||
- name: copy .env.local to apps/readest-app
|
||||
run: cp .env.local apps/readest-app/.env.local
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: contains(matrix.config.os, 'ubuntu')
|
||||
- name: build and upload Android apks
|
||||
if: matrix.config.release == 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/27.0.11902837
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
cd apps/readest-app/
|
||||
rm -rf src-tauri/gen/android
|
||||
pnpm tauri android init
|
||||
pnpm tauri icon ../../data/icons/readest-book.png
|
||||
git checkout .
|
||||
|
||||
pushd src-tauri/gen/android
|
||||
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
||||
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
||||
base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks
|
||||
echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties
|
||||
|
||||
popd
|
||||
version=${{ needs.get-release.outputs.release_version }}
|
||||
apk_path=src-tauri/gen/android/app/build/outputs/apk/universal/release
|
||||
universial_apk=Readest_${version}_universal.apk
|
||||
arm64_apk=Readest_${version}_arm64.apk
|
||||
pnpm tauri android build
|
||||
cp ${apk_path}/app-universal-release.apk $universial_apk
|
||||
pnpm tauri android build -t aarch64
|
||||
cp ${apk_path}/app-universal-release.apk $arm64_apk
|
||||
|
||||
echo "Uploading $universial_apk to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $universial_apk --clobber
|
||||
echo "Uploading $arm64_apk to GitHub release"
|
||||
gh release upload ${{ needs.get-release.outputs.release_tag }} $arm64_apk --clobber
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
if: matrix.config.release != 'android'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
|
||||
Generated
+383
-392
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@
|
||||
|
||||
| **Feature** | **Description** | **Status** |
|
||||
| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental) | ✅ |
|
||||
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
|
||||
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
|
||||
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
|
||||
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.25",
|
||||
"version": "0.9.26",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -56,6 +56,7 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.0",
|
||||
"@tauri-apps/plugin-updater": "^2.5.1",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cssbeautify": "^0.3.1",
|
||||
@@ -65,7 +66,7 @@
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "15.2.2",
|
||||
"next": "15.2.4",
|
||||
"posthog-js": "^1.205.0",
|
||||
"react": "19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
@@ -77,6 +78,7 @@
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "^0.5.12",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
@@ -99,6 +101,7 @@
|
||||
"postcss-nested": "^7.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "التمييزات والتعليقات",
|
||||
"Note": "ملاحظة",
|
||||
"Copied to clipboard": "تم النسخ إلى الحافظة",
|
||||
"Export Annotations": "تصدير التعليقات"
|
||||
"Export Annotations": "تصدير التعليقات",
|
||||
"Auto Import on File Open": "استيراد تلقائي عند فتح الملف",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Hervorhebungen & Anmerkungen",
|
||||
"Note": "Notiz",
|
||||
"Copied to clipboard": "In die Zwischenablage kopiert",
|
||||
"Export Annotations": "Anmerkungen exportieren"
|
||||
"Export Annotations": "Anmerkungen exportieren",
|
||||
"Auto Import on File Open": "Automatischer Import beim Öffnen einer Datei",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Επισημάνσεις & Σχολιασμοί",
|
||||
"Note": "Σημείωση",
|
||||
"Copied to clipboard": "Αντιγράφηκε στο πρόχειρο",
|
||||
"Export Annotations": "Εξαγωγή σχολιασμών"
|
||||
"Export Annotations": "Εξαγωγή σχολιασμών",
|
||||
"Auto Import on File Open": "Αυτόματη εισαγωγή κατά το άνοιγμα αρχείου",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Resaltados y anotaciones",
|
||||
"Note": "Nota",
|
||||
"Copied to clipboard": "Copiado al portapapeles",
|
||||
"Export Annotations": "Exportar anotaciones"
|
||||
"Export Annotations": "Exportar anotaciones",
|
||||
"Auto Import on File Open": "Importación automática al abrir archivo",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Surlignages et annotations",
|
||||
"Note": "Note",
|
||||
"Copied to clipboard": "Copié dans le presse-papiers",
|
||||
"Export Annotations": "Exporter les annotations"
|
||||
"Export Annotations": "Exporter les annotations",
|
||||
"Auto Import on File Open": "Importation automatique à l'ouverture du fichier",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "हाइलाइट और टिप्पणियाँ",
|
||||
"Note": "टिप्पणी",
|
||||
"Copied to clipboard": "क्लिपबोर्ड पर कॉपी किया गया",
|
||||
"Export Annotations": "टिप्पणियाँ निर्यात करें"
|
||||
"Export Annotations": "टिप्पणियाँ निर्यात करें",
|
||||
"Auto Import on File Open": "फ़ाइल खोलने पर स्वचालित आयात",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Sorotan & Anotasi",
|
||||
"Note": "Catatan",
|
||||
"Copied to clipboard": "Disalin ke papan klip",
|
||||
"Export Annotations": "Ekspor Anotasi"
|
||||
"Export Annotations": "Ekspor Anotasi",
|
||||
"Auto Import on File Open": "Impor Otomatis saat File Dibuka",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Evidenziazioni e annotazioni",
|
||||
"Note": "Nota",
|
||||
"Copied to clipboard": "Copiato negli appunti",
|
||||
"Export Annotations": "Esporta annotazioni"
|
||||
"Export Annotations": "Esporta annotazioni",
|
||||
"Auto Import on File Open": "Importazione automatica all'apertura del file",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "ハイライトと注釈",
|
||||
"Note": "メモ",
|
||||
"Copied to clipboard": "クリップボードにコピー",
|
||||
"Export Annotations": "注釈をエクスポート"
|
||||
"Export Annotations": "注釈をエクスポート",
|
||||
"Auto Import on File Open": "ファイルを開くと自動インポート",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "하이라이트 및 주석",
|
||||
"Note": "노트",
|
||||
"Copied to clipboard": "클립보드에 복사",
|
||||
"Export Annotations": "주석 내보내기"
|
||||
"Export Annotations": "주석 내보내기",
|
||||
"Auto Import on File Open": "파일 열 때 자동 가져오기",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Zaznaczenia i adnotacje",
|
||||
"Note": "Notatka",
|
||||
"Copied to clipboard": "Skopiowano do schowka",
|
||||
"Export Annotations": "Eksportuj adnotacje"
|
||||
"Export Annotations": "Eksportuj adnotacje",
|
||||
"Auto Import on File Open": "Automatyczne importowanie przy otwieraniu pliku",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Destaques e Anotações",
|
||||
"Note": "Nota",
|
||||
"Copied to clipboard": "Copiado para a área de transferência",
|
||||
"Export Annotations": "Exportar Anotações"
|
||||
"Export Annotations": "Exportar Anotações",
|
||||
"Auto Import on File Open": "Importação Automática ao Abrir Arquivo",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Выделения и аннотации",
|
||||
"Note": "Заметка",
|
||||
"Copied to clipboard": "Скопировано в буфер обмена",
|
||||
"Export Annotations": "Экспортировать аннотации"
|
||||
"Export Annotations": "Экспортировать аннотации",
|
||||
"Auto Import on File Open": "Автоматический импорт при открытии файла",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Vurgular ve Notlar",
|
||||
"Note": "Not",
|
||||
"Copied to clipboard": "Panoya kopyalandı",
|
||||
"Export Annotations": "Notları Dışa Aktar"
|
||||
"Export Annotations": "Notları Dışa Aktar",
|
||||
"Auto Import on File Open": "Dosya Açıldığında Otomatik İçe Aktar",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Виділення та анотації",
|
||||
"Note": "Примітка",
|
||||
"Copied to clipboard": "Скопійовано в буфер обміну",
|
||||
"Export Annotations": "Експортувати анотації"
|
||||
"Export Annotations": "Експортувати анотації",
|
||||
"Auto Import on File Open": "Автоматичний імпорт при відкритті файлу",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "Điểm nổi bật & Chú thích",
|
||||
"Note": "Ghi chú",
|
||||
"Copied to clipboard": "Đã sao chép vào bảng ghi",
|
||||
"Export Annotations": "Xuất chú thích"
|
||||
"Export Annotations": "Xuất chú thích",
|
||||
"Auto Import on File Open": "Tự động nhập khi mở tệp",
|
||||
"LXGW WenKai GB Screen": "LXGW WenKai"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "划线和笔记",
|
||||
"Note": "笔记",
|
||||
"Copied to clipboard": "已复制到剪贴板",
|
||||
"Export Annotations": "导出笔记"
|
||||
"Export Annotations": "导出笔记",
|
||||
"Auto Import on File Open": "打开文件时自动导入",
|
||||
"LXGW WenKai GB Screen": "霞鹜文楷"
|
||||
}
|
||||
|
||||
@@ -258,5 +258,7 @@
|
||||
"Highlights & Annotations": "高亮和筆記",
|
||||
"Note": "筆記",
|
||||
"Copied to clipboard": "已複製到剪貼板",
|
||||
"Export Annotations": "匯出筆記"
|
||||
"Export Annotations": "匯出筆記",
|
||||
"Auto Import on File Open": "打開文件時自動導入",
|
||||
"LXGW WenKai GB Screen": "霞鶩文楷"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.26": {
|
||||
"date": "2025-03-27",
|
||||
"notes": [
|
||||
"Support open book from file manager without importing",
|
||||
"Add LXGW WenKai font in Serif fonts list",
|
||||
"Various fixes on layout and styles on Windows and Android"
|
||||
]
|
||||
},
|
||||
"0.9.25": {
|
||||
"date": "2025-03-24",
|
||||
"notes": [
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.readest"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="web.readest.com" />
|
||||
|
||||
</intent-filter>
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
+2
-2
@@ -49,7 +49,7 @@
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -111,7 +111,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
||||
@@ -63,6 +63,15 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn set_rounded_window(app: &AppHandle, rounded: bool) {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let script = format!("window.IS_ROUNDED = {};", rounded);
|
||||
if let Err(e) = window.eval(&script) {
|
||||
eprintln!("Failed to set IS_ROUNDED variable: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn start_server(window: Window) -> Result<u16, String> {
|
||||
start(move |url| {
|
||||
@@ -185,6 +194,12 @@ pub fn run() {
|
||||
app_handle.get_webview_window("main").unwrap()
|
||||
.eval("window.__READEST_CLI_ACCESS = true; window.__READEST_UPDATER_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
|
||||
set_rounded_window(&app_handle, true);
|
||||
#[cfg(target_os = "windows")]
|
||||
if tauri_plugin_os::version() <= tauri_plugin_os::Version::from_string("10.0.19045") {
|
||||
set_rounded_window(&app_handle, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,8 +240,7 @@ pub fn run() {
|
||||
.title("Readest");
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
let version = tauri_plugin_os::version();
|
||||
if version <= tauri_plugin_os::Version::from_string("10.0.19045") {
|
||||
if tauri_plugin_os::version() <= tauri_plugin_os::Version::from_string("10.0.19045") {
|
||||
win_builder = win_builder.shadow(false);
|
||||
} else {
|
||||
win_builder = win_builder.shadow(true);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
|
||||
import { authWithSafari } from './utils/safariAuth';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
|
||||
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
|
||||
|
||||
@@ -47,11 +48,11 @@ const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Ic
|
||||
onClick={() => handleSignIn(provider)}
|
||||
className={clsx(
|
||||
'mb-2 flex w-64 items-center justify-center rounded border p-2.5',
|
||||
'bg-base-100 border-gray-300 shadow-sm transition hover:bg-gray-50',
|
||||
'bg-base-100 border-base-300 hover:bg-base-200 shadow-sm transition',
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
<span className='text-neutral-content/70 px-2 text-sm'>{label}</span>
|
||||
<span className='text-base-content/75 px-2 text-sm'>{label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -67,6 +68,8 @@ export default function AuthPage() {
|
||||
const isOAuthServerRunning = useRef(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getTauriRedirectTo = (isOAuth: boolean) => {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
|
||||
if (appService?.isIOSApp) {
|
||||
@@ -298,21 +301,31 @@ export default function AuthPage() {
|
||||
return isTauriAppPlatform() ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-6 flex',
|
||||
'bg-base-100 border-base-200 flex h-dvh w-full select-none flex-col items-center border',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'btn btn-ghost fixed left-4 h-8 min-h-8 w-8 p-0',
|
||||
appService?.hasSafeAreaInset && 'top-[calc(env(safe-area-inset-top)+16px)]',
|
||||
appService?.hasTrafficLight && 'top-11',
|
||||
'flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<button onClick={handleGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={appService?.hasWindowBar}
|
||||
showMaximize={appService?.hasWindowBar}
|
||||
showClose={appService?.hasWindowBar}
|
||||
onClose={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
@@ -332,7 +345,7 @@ export default function AuthPage() {
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='my-3 mt-6 w-64 border-t border-gray-200' />
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
|
||||
@@ -68,7 +68,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar h-13 z-10 flex w-full items-center py-2 pr-4 sm:pr-6',
|
||||
'titlebar z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:pr-6',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PiUserCircleCheck } from 'react-icons/pi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { hasUpdater, isWebAppPlatform } from '@/services/environment';
|
||||
import { hasUpdater, isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -33,6 +33,9 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
|
||||
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
|
||||
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
|
||||
settings.autoImportBooksOnOpen,
|
||||
);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
setAboutDialogVisible(true);
|
||||
@@ -74,6 +77,13 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoImportBooksOnOpen = () => {
|
||||
settings.autoImportBooksOnOpen = !settings.autoImportBooksOnOpen;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsAutoImportBooksOnOpen(settings.autoImportBooksOnOpen);
|
||||
};
|
||||
|
||||
const toggleAutoCheckUpdates = () => {
|
||||
settings.autoCheckUpdates = !settings.autoCheckUpdates;
|
||||
setSettings(settings);
|
||||
@@ -150,6 +160,13 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
icon={isAutoUpload ? <MdCheck className='text-base-content' /> : undefined}
|
||||
onClick={toggleAutoUploadBooks}
|
||||
/>
|
||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||
<MenuItem
|
||||
label={_('Auto Import on File Open')}
|
||||
icon={isAutoImportBooksOnOpen ? <MdCheck className='text-base-content' /> : undefined}
|
||||
onClick={toggleAutoImportBooksOnOpen}
|
||||
/>
|
||||
)}
|
||||
{hasUpdater() && (
|
||||
<MenuItem
|
||||
label={_('Check Updates on Start')}
|
||||
@@ -163,9 +180,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
{appService?.hasRoundedWindow && (
|
||||
<MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />
|
||||
)}
|
||||
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
|
||||
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
|
||||
@@ -191,11 +191,13 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
const processOpenWithFiles = React.useCallback(
|
||||
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
|
||||
const settings = await appService.loadSettings();
|
||||
const bookIds: string[] = [];
|
||||
for (const file of openWithFiles) {
|
||||
console.log('Open with book:', file);
|
||||
try {
|
||||
const book = await appService.importBook(file, libraryBooks);
|
||||
const temp = !settings.autoImportBooksOnOpen;
|
||||
const book = await appService.importBook(file, libraryBooks, true, true, false, temp);
|
||||
if (book) {
|
||||
bookIds.push(book.hash);
|
||||
}
|
||||
@@ -508,8 +510,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'scroll-container drop-zone mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
|
||||
'scroll-container drop-zone mt-[52px] flex-grow overflow-y-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(52px+env(safe-area-inset-top))]',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
|
||||
isDragging && 'drag-over',
|
||||
)}
|
||||
|
||||
@@ -45,7 +45,7 @@ const PageInfoView: React.FC<PageInfoProps> = ({
|
||||
className={clsx(
|
||||
'pageinfo absolute bottom-0 flex items-center justify-end',
|
||||
isVertical ? 'writing-vertical-rl' : 'h-12 w-full',
|
||||
isScrolled && 'bg-base-100',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
|
||||
@@ -23,7 +23,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
className={clsx(
|
||||
'sectioninfo absolute flex items-center overflow-hidden',
|
||||
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
|
||||
isScrolled && 'bg-base-100',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
isVertical
|
||||
|
||||
@@ -132,9 +132,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
<hr className='border-base-300 my-1' />
|
||||
|
||||
{appService?.hasRoundedWindow && (
|
||||
<MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />
|
||||
)}
|
||||
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
|
||||
<MenuItem
|
||||
label={
|
||||
themeMode === 'dark'
|
||||
|
||||
@@ -77,7 +77,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const dictPopupHeight = Math.min(300, maxHeight);
|
||||
const transPopupWidth = Math.min(480, maxWidth);
|
||||
const transPopupHeight = Math.min(360, maxHeight);
|
||||
const annotPopupWidth = useResponsiveSize(300);
|
||||
const annotPopupWidth = Math.min(useResponsiveSize(300), maxWidth);
|
||||
const annotPopupHeight = useResponsiveSize(44);
|
||||
const androidSelectionHandlerHeight = 0;
|
||||
|
||||
@@ -448,6 +448,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { bookDoc, book } = bookData;
|
||||
if (!bookDoc || !book || !bookDoc.toc) return;
|
||||
|
||||
const config = getConfig(bookKey)!;
|
||||
const { booknotes: allNotes = [] } = config;
|
||||
const booknotes = allNotes.filter((note) => !note.deletedAt);
|
||||
if (booknotes.length === 0) {
|
||||
|
||||
@@ -44,19 +44,22 @@ const FontFace = ({
|
||||
moreOptions,
|
||||
selected,
|
||||
onSelect,
|
||||
}: FontFaceProps) => (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className=''>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options.map((option) => ({ option, label: option }))}
|
||||
moreOptions={moreOptions?.map((option) => ({ option, label: option })) ?? []}
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
onGetFontFamily={handleFontFaceFont}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}: FontFaceProps) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className=''>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options.map((option) => ({ option, label: _(option) }))}
|
||||
moreOptions={moreOptions?.map((option) => ({ option, label: option })) ?? []}
|
||||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
onGetFontFamily={handleFontFaceFont}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import i18n from 'i18next';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
@@ -14,7 +14,7 @@ import DropDown from './DropDown';
|
||||
|
||||
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
@@ -26,6 +26,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [inputFocusInAndroid, setInputFocusInAndroid] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const handleUserStylesheetChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const cssInput = e.target.value;
|
||||
setDraftStylesheet(cssInput);
|
||||
@@ -72,6 +75,24 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const handleInputFocus = () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
setInputFocusInAndroid(true);
|
||||
}
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'center',
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
setInputFocusInAndroid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentUILangOption = () => {
|
||||
const uiLanguage = viewSettings.uiLanguage;
|
||||
return {
|
||||
@@ -118,7 +139,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
}, [isContinuousScroll]);
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div
|
||||
className={clsx('my-4 w-full space-y-6', inputFocusInAndroid && 'h-[50%] overflow-y-auto')}
|
||||
>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Language')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
@@ -185,6 +208,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
>
|
||||
<div className='relative p-1'>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={clsx(
|
||||
'textarea textarea-ghost h-48 w-full border-0 p-3 text-base !outline-none sm:text-sm',
|
||||
'placeholder:text-base-content/70',
|
||||
@@ -192,6 +216,8 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
placeholder={_('Enter your custom CSS here...')}
|
||||
spellCheck='false'
|
||||
value={draftStylesheet}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInput}
|
||||
onKeyUp={handleInput}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { IoArrowBack } from 'react-icons/io5';
|
||||
@@ -17,6 +17,7 @@ import { navigateToLibrary } from '@/utils/nav';
|
||||
import { deleteUser } from '@/libs/user';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Quota from '@/components/Quota';
|
||||
|
||||
const ProfilePage = () => {
|
||||
@@ -29,6 +30,8 @@ const ProfilePage = () => {
|
||||
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -154,21 +157,31 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-6 flex justify-center',
|
||||
'bg-base-100 border-base-200 flex h-dvh w-full select-none flex-col items-center border',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'btn btn-ghost fixed left-4 h-8 min-h-8 w-8 p-0',
|
||||
appService?.hasSafeAreaInset && 'top-[calc(env(safe-area-inset-top)+16px)]',
|
||||
appService?.hasTrafficLight && 'top-11',
|
||||
'flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<button onClick={handleGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={appService?.hasWindowBar}
|
||||
showMaximize={appService?.hasWindowBar}
|
||||
showClose={appService?.hasWindowBar}
|
||||
onClose={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full max-w-4xl px-4 py-10'>
|
||||
<div className='bg-base-200 overflow-hidden rounded-lg p-2 shadow-md sm:p-6'>
|
||||
<div className='p-2 sm:p-6'>
|
||||
|
||||
@@ -64,8 +64,8 @@ export const Toast = () => {
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'max-h-[50vh] min-w-[30vw] max-w-80',
|
||||
'overflow-scroll whitespace-normal break-words text-center',
|
||||
'max-h-[50vh] min-w-32 max-w-80',
|
||||
'overflow-y-auto whitespace-normal break-words text-center',
|
||||
messageClass.current,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ export const useUICSS = (bookKey: string, viewSettings: ViewSettings) => {
|
||||
styleElement.remove();
|
||||
}
|
||||
|
||||
const rawCSS = viewSettings.userStylesheet;
|
||||
const rawCSS = viewSettings.userStylesheet || '';
|
||||
const newStyleEl = document.createElement('style');
|
||||
newStyleEl.textContent = rawCSS.replace('foliate-view', `#foliate-view-${bookKey}`);
|
||||
document.head.appendChild(newStyleEl);
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { createSupabaseClient } from '@/utils/supabase';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { s3Client } from '@/utils/s3';
|
||||
import { deleteObject } from '@/utils/r2';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
@@ -41,13 +40,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(403).json({ error: 'Unauthorized access to the file' });
|
||||
}
|
||||
|
||||
const deleteCommand = new DeleteObjectCommand({
|
||||
Bucket: process.env['R2_BUCKET_NAME'] || '',
|
||||
Key: fileKey,
|
||||
});
|
||||
|
||||
try {
|
||||
await s3Client.send(deleteCommand);
|
||||
await deleteObject(process.env['R2_BUCKET_NAME'] || '', fileKey);
|
||||
const { error: deleteError } = await supabase.from('files').delete().eq('id', fileRecord.id);
|
||||
|
||||
if (deleteError) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { supabase, createSupabaseClient } from '@/utils/supabase';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { s3Client } from '@/utils/s3';
|
||||
import { getDownloadSignedUrl } from '@/utils/r2';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
@@ -56,15 +54,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(403).json({ error: 'Unauthorized access to the file' });
|
||||
}
|
||||
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: process.env['R2_BUCKET_NAME'] || '',
|
||||
Key: fileKey,
|
||||
});
|
||||
|
||||
try {
|
||||
const downloadUrl = await getSignedUrl(s3Client, getCommand, {
|
||||
expiresIn: 1800,
|
||||
});
|
||||
const bucketName = process.env['R2_BUCKET_NAME'] || '';
|
||||
const downloadUrl = await getDownloadSignedUrl(bucketName, fileKey, 1800);
|
||||
|
||||
res.status(200).json({
|
||||
downloadUrl,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { supabase, createSupabaseClient } from '@/utils/supabase';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { s3Client } from '@/utils/s3';
|
||||
import { getUploadSignedUrl } from '@/utils/r2';
|
||||
|
||||
const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
if (!authHeader) return {};
|
||||
@@ -75,19 +73,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (insertError) return res.status(500).json({ error: insertError.message });
|
||||
}
|
||||
|
||||
const signableHeaders = new Set<string>();
|
||||
signableHeaders.add('content-length');
|
||||
const putCommand = new PutObjectCommand({
|
||||
Bucket: process.env['R2_BUCKET_NAME'] || '',
|
||||
Key: objectKey,
|
||||
ContentLength: objSize,
|
||||
});
|
||||
|
||||
try {
|
||||
const uploadUrl = await getSignedUrl(s3Client, putCommand, {
|
||||
expiresIn: 1800,
|
||||
signableHeaders,
|
||||
});
|
||||
const uploadUrl = await getUploadSignedUrl(
|
||||
process.env['R2_BUCKET_NAME'] || '',
|
||||
objectKey,
|
||||
objSize,
|
||||
1800,
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
uploadUrl,
|
||||
|
||||
@@ -49,6 +49,7 @@ export abstract class BaseAppService implements AppService {
|
||||
abstract isAndroidApp: boolean;
|
||||
abstract isIOSApp: boolean;
|
||||
abstract hasTrafficLight: boolean;
|
||||
abstract hasWindow: boolean;
|
||||
abstract hasWindowBar: boolean;
|
||||
abstract hasContextMenu: boolean;
|
||||
abstract hasRoundedWindow: boolean;
|
||||
@@ -131,6 +132,7 @@ export abstract class BaseAppService implements AppService {
|
||||
saveBook: boolean = true,
|
||||
saveCover: boolean = true,
|
||||
overwrite: boolean = false,
|
||||
transient: boolean = false,
|
||||
): Promise<Book | null> {
|
||||
try {
|
||||
let loadedBook: BookDoc;
|
||||
@@ -162,7 +164,7 @@ export abstract class BaseAppService implements AppService {
|
||||
const hash = await partialMD5(fileobj);
|
||||
const existingBook = books.filter((b) => b.hash === hash)[0];
|
||||
if (existingBook) {
|
||||
if (existingBook.deletedAt) {
|
||||
if (!transient) {
|
||||
existingBook.deletedAt = null;
|
||||
}
|
||||
existingBook.updatedAt = Date.now();
|
||||
@@ -175,6 +177,7 @@ export abstract class BaseAppService implements AppService {
|
||||
author: formatAuthors(loadedBook.metadata.author, loadedBook.metadata.language),
|
||||
createdAt: existingBook ? existingBook.createdAt : Date.now(),
|
||||
uploadedAt: existingBook ? existingBook.uploadedAt : null,
|
||||
deletedAt: transient ? Date.now() : null,
|
||||
downloadedAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { ReadSettings, SystemSettings } from '@/types/settings';
|
||||
import { UserStorageQuota } from '@/types/user';
|
||||
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
|
||||
@@ -35,6 +36,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
autoUpload: true,
|
||||
autoCheckUpdates: true,
|
||||
screenWakeLock: true,
|
||||
autoImportBooksOnOpen: false,
|
||||
|
||||
lastSyncedAtBooks: 0,
|
||||
lastSyncedAtConfigs: 0,
|
||||
@@ -106,6 +108,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
fullJustification: false,
|
||||
animated: true,
|
||||
defaultFont: 'Sans-serif',
|
||||
};
|
||||
|
||||
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
|
||||
@@ -139,6 +142,7 @@ export const SERIF_FONTS = [
|
||||
'Vollkorn',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
_('LXGW WenKai GB Screen'),
|
||||
];
|
||||
|
||||
export const SANS_SERIF_FONTS = ['Roboto', 'Noto Sans', 'Open Sans', 'Helvetica', 'Arial'];
|
||||
|
||||
@@ -23,6 +23,12 @@ import { isValidURL } from '@/utils/misc';
|
||||
import { BaseAppService } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
IS_ROUNDED?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const OS_TYPE = osType();
|
||||
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
@@ -122,9 +128,10 @@ export class NativeAppService extends BaseAppService {
|
||||
isAndroidApp = OS_TYPE === 'android';
|
||||
isIOSApp = OS_TYPE === 'ios';
|
||||
hasTrafficLight = OS_TYPE === 'macos';
|
||||
hasWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasWindowBar = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasContextMenu = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android') && !!window.IS_ROUNDED;
|
||||
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
hasSysFontsList = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
|
||||
@@ -184,6 +184,7 @@ export class WebAppService extends BaseAppService {
|
||||
isAndroidApp = false;
|
||||
isIOSApp = false;
|
||||
hasTrafficLight = false;
|
||||
hasWindow = false;
|
||||
hasWindowBar = false;
|
||||
hasContextMenu = false;
|
||||
hasRoundedWindow = false;
|
||||
|
||||
@@ -209,7 +209,7 @@ foliate-view {
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface SystemSettings {
|
||||
autoUpload: boolean;
|
||||
autoCheckUpdates: boolean;
|
||||
screenWakeLock: boolean;
|
||||
autoImportBooksOnOpen: boolean;
|
||||
|
||||
lastSyncedAtBooks: number;
|
||||
lastSyncedAtConfigs: number;
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface AppService {
|
||||
osPlatform: string;
|
||||
appPlatform: AppPlatform;
|
||||
hasTrafficLight: boolean;
|
||||
hasWindow: boolean;
|
||||
hasWindowBar: boolean;
|
||||
hasContextMenu: boolean;
|
||||
hasRoundedWindow: boolean;
|
||||
@@ -47,6 +48,7 @@ export interface AppService {
|
||||
saveBook?: boolean,
|
||||
saveCover?: boolean,
|
||||
overwrite?: boolean,
|
||||
transient?: boolean,
|
||||
): Promise<Book | null>;
|
||||
deleteBook(book: Book, includingUploaded?: boolean): Promise<void>;
|
||||
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { AwsClient } from 'aws4fetch';
|
||||
|
||||
const getR2Client = () => {
|
||||
return new AwsClient({
|
||||
service: 's3',
|
||||
region: process.env['R2_REGION'] || 'auto',
|
||||
accessKeyId: process.env['R2_ACCESS_KEY_ID']!,
|
||||
secretAccessKey: process.env['R2_SECRET_ACCESS_KEY']!,
|
||||
});
|
||||
};
|
||||
|
||||
const getR2Url = () => {
|
||||
const R2_ACCOUNT_ID = process.env['R2_ACCOUNT_ID']!;
|
||||
return `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
||||
};
|
||||
|
||||
export const getDownloadSignedUrl = async (
|
||||
bucketName: string,
|
||||
fileKey: string,
|
||||
expiresIn: number,
|
||||
) => {
|
||||
return (
|
||||
await getR2Client().sign(
|
||||
new Request(`${getR2Url()}/${bucketName}/${fileKey}?X-Amz-Expires=${expiresIn}`),
|
||||
{
|
||||
aws: { signQuery: true },
|
||||
},
|
||||
)
|
||||
).url.toString();
|
||||
};
|
||||
|
||||
export const getUploadSignedUrl = async (
|
||||
bucketName: string,
|
||||
fileKey: string,
|
||||
contentLength: number,
|
||||
expiresIn: number,
|
||||
) => {
|
||||
return (
|
||||
await getR2Client().sign(
|
||||
new Request(
|
||||
`${getR2Url()}/${bucketName}/${fileKey}?X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=content-length`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Length': contentLength.toString(),
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
aws: { signQuery: true },
|
||||
},
|
||||
)
|
||||
).url.toString();
|
||||
};
|
||||
|
||||
export const deleteObject = async (bucketName: string, fileKey: string) => {
|
||||
return await getR2Client().fetch(`${getR2Url()}/${bucketName}/${fileKey}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
@@ -76,6 +76,7 @@ const getFontStyles = (
|
||||
|
||||
const getAdditionalFontLinks = () => `
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans-webfont@1.0.4/misans-l3/misans-l3/result.min.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cn-fontsource-lxgw-wen-kai-gb-screen@1.0.6/font.min.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP&display=swap" crossorigin="anonymous">
|
||||
`;
|
||||
|
||||
@@ -145,7 +146,7 @@ const getLayoutStyles = (
|
||||
color: ${fg};
|
||||
}
|
||||
a:any-link {
|
||||
color: ${primary} ${bg === '#ffffff' ? '' : '!important'};
|
||||
color: ${primary} ${isDarkMode ? '!important' : ''};
|
||||
}
|
||||
aside[epub|type~="footnote"] {
|
||||
display: none;
|
||||
@@ -189,8 +190,8 @@ const getLayoutStyles = (
|
||||
background-color: var(--theme-bg-color, transparent);
|
||||
background: var(--background-set, none);
|
||||
}
|
||||
body *:not(a):not(#b1):not(#b1 *):not(#b2):not(#b2 *):not(.bg):not(.bg *):not(.vol):not(.vol *):not(.background):not(.background *) {
|
||||
${bg === '#ffffff' ? '' : `background-color: ${bg} !important;`}
|
||||
body *:not(a):not(#b1):not(#b1 *):not(#b2):not(#b2 *):not(img):not(.bg):not(.bg *):not(.vol):not(.vol *):not(.background):not(.background *) {
|
||||
${isDarkMode ? `background-color: ${bg} !important;` : ''}
|
||||
}
|
||||
body {
|
||||
overflow: unset;
|
||||
|
||||
@@ -4,7 +4,6 @@ const supabaseUrl =
|
||||
process.env['NEXT_PUBLIC_SUPABASE_URL'] || process.env['NEXT_PUBLIC_DEV_SUPABASE_URL']!;
|
||||
const supabaseAnonKey =
|
||||
process.env['NEXT_PUBLIC_SUPABASE_ANON_KEY'] || process.env['NEXT_PUBLIC_DEV_SUPABASE_ANON_KEY']!;
|
||||
const supabaseAdminKey = process.env['SUPABASE_ADMIN_KEY'] || '';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
@@ -21,6 +20,7 @@ export const createSupabaseClient = (accessToken?: string) => {
|
||||
};
|
||||
|
||||
export const createSupabaseAdminClient = () => {
|
||||
const supabaseAdminKey = process.env['SUPABASE_ADMIN_KEY'] || '';
|
||||
return createClient(supabaseUrl, supabaseAdminKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
+1
-1
Submodule packages/foliate-js updated: ef67dd9801...87c2aaf5c5
+1
-1
Submodule packages/tauri updated: 2d029a9f53...f235ec0113
Generated
+3233
-50
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user