Compare commits

...

15 Commits

Author SHA1 Message Date
Huang Xin 6d4ad34e32 release: version 0.9.26 (#723) 2025-03-26 17:44:26 +01:00
Huang Xin 3239ff0a80 feat: add LXGW WenKai to serif fonts list (#722) 2025-03-26 17:28:14 +01:00
Huang Xin 8b7e3c6f79 fix: textarea hidden behind keyboard in fullscreen Android app. closes #653 (#720) 2025-03-26 15:57:35 +01:00
Huang Xin ace4b78420 fix: uncrash scroll prev at start and scroll next at end (#712) 2025-03-26 12:42:39 +01:00
Huang Xin 39ac89fead fix: don't override bg color for img, closes #656 (#711) 2025-03-26 12:23:46 +01:00
Huang Xin 490fe7e70c ui: fix library header padding (#710) 2025-03-26 09:07:38 +01:00
Huang Xin 06a7a8ea82 feat: add option for transient import from file managers, closes #456 (#709) 2025-03-26 07:11:47 +01:00
Huang Xin b7f4a3503f doc: add TXT to supported formats list (#708)
* fix: set max width for annotation toolbar

* doc: add TXT to supported formats list
2025-03-26 11:45:56 +08:00
Huang Xin 835ed443a5 fix: style tweaks for Windows (#707) 2025-03-25 18:58:23 +01:00
Huang Xin 3f3c6983ba fix: also export newly created annotations (#704) 2025-03-25 06:26:11 +01:00
Huang Xin f22ce1c225 fix: access runtime variables in function invoke (#703) 2025-03-25 05:29:22 +01:00
Huang Xin 9990de112f chore: bump updates of next.js and tauri (#702) 2025-03-25 04:46:07 +01:00
Huang Xin ede37757db chore: support deploy in cloudflare with opennext and fix android build 2025-03-25 03:52:04 +08:00
Huang Xin 0c65d44bc9 chore: add android release in github actions (#695) 2025-03-25 01:57:05 +08:00
Huang Xin 07c9813ec1 fix: no background color for section and page info in vertical scroll mode (#694) 2025-03-24 07:13:38 +01:00
57 changed files with 4046 additions and 575 deletions
+63 -8
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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. | ✅ |
+6 -3
View File
@@ -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": "霞鶩文楷"
}
+8
View File
@@ -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>
@@ -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"
+16 -2
View File
@@ -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);
+25 -12
View File
@@ -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} />}
+5 -3
View File
@@ -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}
+23 -10
View File
@@ -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'>
+2 -2
View File
@@ -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,
)}
>
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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;
+1 -1
View File
@@ -209,7 +209,7 @@ foliate-view {
}
.scroll-container {
overflow-y: scroll;
overflow-y: auto;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
+1
View File
@@ -24,6 +24,7 @@ export interface SystemSettings {
autoUpload: boolean;
autoCheckUpdates: boolean;
screenWakeLock: boolean;
autoImportBooksOnOpen: boolean;
lastSyncedAtBooks: number;
lastSyncedAtConfigs: number;
+2
View File
@@ -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>;
+60
View File
@@ -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',
});
};
+4 -3
View File
@@ -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;
+1 -1
View File
@@ -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

+3233 -50
View File
File diff suppressed because it is too large Load Diff