forked from akai/readest
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36e09582bc | |||
| ccc04825b7 | |||
| 9e7e41a623 | |||
| 1ff929ece5 | |||
| 4c7b7881c8 | |||
| 1d5b189185 | |||
| a9aa2fe7a4 | |||
| 0253afca86 | |||
| 7ddbaa644b | |||
| e96782baab | |||
| 66784736ee | |||
| 2bb6ed2602 | |||
| 48ef2d634f |
@@ -2,12 +2,6 @@ name: Release Readest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_ubuntu_arm:
|
||||
description: 'Run only ubuntu-22.04-arm'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -83,7 +77,6 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
timeout-minutes: 30
|
||||
if: ${{ github.event.inputs.test_arm_only }} != 'true' || matrix.config.os == 'ubuntu-22.04-arm'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -131,7 +124,7 @@ jobs:
|
||||
if: contains(matrix.config.os, 'ubuntu')
|
||||
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
|
||||
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
|
||||
Generated
+26
@@ -23,12 +23,14 @@ dependencies = [
|
||||
"tauri-plugin-devtools",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-haptics",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-oauth",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-safari-auth",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-sign-in-with-apple",
|
||||
"tauri-plugin-single-instance",
|
||||
@@ -5207,6 +5209,20 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-haptics"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52db8ae1bb6eab8502ee5c32acb905323c9651491c7f229d84f45b8c57d12327"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.3.0"
|
||||
@@ -5316,6 +5332,16 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-safari-auth"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.2.0"
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
FROM node:22-slim
|
||||
|
||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||
ENV PATH="${PATH}:${PNPM_HOME}"
|
||||
|
||||
RUN npm install --global pnpm
|
||||
|
||||
# Install necessary packages
|
||||
RUN apt update -y && apt install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Rust and Cargo
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
RUN pnpm --filter @readest/readest-app setup-pdfjs
|
||||
|
||||
WORKDIR /app/apps/readest-app
|
||||
|
||||
RUN pnpm build-web
|
||||
|
||||
ENTRYPOINT ["pnpm", "start-web"]
|
||||
@@ -4,8 +4,8 @@ NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
|
||||
DEEPL_PRO_API_KEY=YOUR_DEEPL_PRO_API_KEY
|
||||
DEEPL_FREE_API_KEY=YOUR_DEEPL_FREE_API_KEY
|
||||
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
|
||||
DEEPL_FREE_API_KEYS=YOUR_DEEPL_FREE_API_KEYS
|
||||
|
||||
R2_TOKEN_VALUE=YOUR_R2_TOKEN_VALUE
|
||||
R2_ACCESS_KEY_ID=YOUR_R2_ACCESS_KEY_ID
|
||||
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
'hi',
|
||||
'id',
|
||||
'vi',
|
||||
'ar',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.15",
|
||||
"version": "0.9.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -47,6 +47,7 @@
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-fs": "^2.2.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.3",
|
||||
"@tauri-apps/plugin-http": "^2.3.0",
|
||||
"@tauri-apps/plugin-log": "^2.2.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.5",
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"(detected)": "(تم التعرف)",
|
||||
"About Readest": "حول ريديست",
|
||||
"Add your notes here...": "أضف ملاحظاتك هنا...",
|
||||
"Animation": "الرسوم المتحركة",
|
||||
"Annotate": "إضافة ملاحظة",
|
||||
"Apply": "تطبيق",
|
||||
"Are you sure to delete the selected books?": "هل أنت متأكد من رغبتك في حذف الكتب المحددة؟",
|
||||
"Auto Mode": "الوضع التلقائي",
|
||||
"Behavior": "السلوك",
|
||||
"Book": "الكتاب",
|
||||
"Book Cover": "غلاف الكتاب",
|
||||
"Bookmark": "علامة مرجعية",
|
||||
"Cancel": "إلغاء",
|
||||
"Chapter": "الفصل",
|
||||
"Cherry": "كرزي",
|
||||
"Color": "اللون",
|
||||
"Confirm": "تأكيد",
|
||||
"Confirm Deletion": "تأكيد الحذف",
|
||||
"Copied to notebook": "تم النسخ إلى المفكرة",
|
||||
"Copy": "نسخ",
|
||||
"Custom CSS": "CSS مخصص",
|
||||
"Dark Mode": "الوضع المظلم",
|
||||
"Default": "الافتراضي",
|
||||
"Default Font": "الخط الافتراضي",
|
||||
"Default Font Size": "حجم الخط الافتراضي",
|
||||
"Delete": "حذف",
|
||||
"Delete Highlight": "حذف التمييز",
|
||||
"Dictionary": "القاموس",
|
||||
"Disable Click-to-Flip": "تعطيل التقليب بالنقر",
|
||||
"Download Readest": "تحميل ريديست",
|
||||
"Edit": "تحرير",
|
||||
"Enter your custom CSS here...": "أدخل الـ CSS المخصص هنا...",
|
||||
"Excerpts": "مقتطفات",
|
||||
"Failed to import book(s): {{filenames}}": "فشل استيراد الكتاب/الكتب: {{filenames}}",
|
||||
"Fast": "سريع",
|
||||
"Font": "الخط",
|
||||
"Font & Layout": "الخط والتخطيط",
|
||||
"Font Face": "نوع الخط",
|
||||
"Font Family": "عائلة الخط",
|
||||
"Font Size": "حجم الخط",
|
||||
"Full Justification": "محاذاة كاملة",
|
||||
"Global Settings": "الإعدادات العامة",
|
||||
"Go Back": "العودة",
|
||||
"Go Forward": "التقدم",
|
||||
"Go Left": "الذهاب لليسار",
|
||||
"Go Right": "الذهاب لليمين",
|
||||
"Grass": "عشبي",
|
||||
"Gray": "رمادي",
|
||||
"Gruvbox": "چروفبوكس",
|
||||
"Highlight": "تمييز",
|
||||
"Horizontal Direction": "الاتجاه الأفقي",
|
||||
"Hyphenation": "الواصلة",
|
||||
"Identifier:": "المعرف:",
|
||||
"Import Books": "استيراد الكتب",
|
||||
"Invert Colors in Dark Mode": "عكس الألوان في الوضع المظلم",
|
||||
"Language:": "اللغة:",
|
||||
"Layout": "التخطيط",
|
||||
"Light Mode": "الوضع الفاتح",
|
||||
"Loading...": "جارٍ التحميل...",
|
||||
"Loc. {{currentPage}} / {{totalPage}}": "الصفحة {{currentPage}} \\ {{totalPage}}",
|
||||
"Logged in": "تم تسجيل الدخول",
|
||||
"Logged in as {{userDisplayName}}": "تم تسجيل الدخول باسم {{userDisplayName}}",
|
||||
"Match Case": "مطابقة حالة الأحرف",
|
||||
"Match Diacritics": "مطابقة التشكيل",
|
||||
"Match Whole Words": "مطابقة الكلمات الكاملة",
|
||||
"Maximum Block Size": "أقصى مساحة رأسية",
|
||||
"Maximum Inline Size": "أقصى مساحة أفقية",
|
||||
"Maximum Number of Columns": "العدد الأقصى للأعمدة",
|
||||
"Minimum Font Size": "الحد الأدنى لحجم الخط",
|
||||
"Misc": "أخرى",
|
||||
"Monospace Font": "خط أحادي المسافة (Monospace)",
|
||||
"More Info": "مزيد من المعلومات",
|
||||
"Nord": "نورد",
|
||||
"Notebook": "المفكرة",
|
||||
"Notes": "الملاحظات",
|
||||
"Open": "فتح",
|
||||
"Original Text": "النص الأصلي",
|
||||
"Page": "الصفحة",
|
||||
"Paging Animation": "رسوم متحركةعند الانتقال إلى صفحة جديدة",
|
||||
"Paragraph": "فقرة",
|
||||
"Parallel Read": "قراءة متوازية",
|
||||
"Published:": "تاريخ النشر:",
|
||||
"Publisher:": "الناشر:",
|
||||
"Reading Progress Synced": "تمت مزامنة تقدم القراءة",
|
||||
"Reload Page": "إعادة تحميل الصفحة",
|
||||
"Reveal in File Explorer": "إظهار في مستكشف الملفات",
|
||||
"Reveal in Finder": "إظهار في Finder",
|
||||
"Reveal in Folder": "إظهار في المجلد",
|
||||
"Sans-Serif Font": "خط غير مذيل (Sans-Serif)",
|
||||
"Save": "حفظ",
|
||||
"Scrolled Mode": "وضع التمرير",
|
||||
"Search": "البحث",
|
||||
"Search Books...": "بحث في الكتب...",
|
||||
"Search...": "بحث...",
|
||||
"Select Book": "تحديد الكتاب",
|
||||
"Select Books": "تحديد الكتب",
|
||||
"Select Multiple Books": "تحديد عدة كتب",
|
||||
"Sepia": "بني داكن",
|
||||
"Serif Font": "خط مذيل (Serif)",
|
||||
"Show Book Details": "عرض تفاصيل الكتاب",
|
||||
"Sidebar": "الشريط الجانبي",
|
||||
"Sign In": "تسجيل الدخول",
|
||||
"Sign Out": "تسجيل الخروج",
|
||||
"Sky": "سماوي",
|
||||
"Slow": "بطيء",
|
||||
"Solarized": "سولاريزد",
|
||||
"Speak": "تحدث",
|
||||
"Subjects:": "المواضيع:",
|
||||
"System Fonts": "خطوط النظام",
|
||||
"TTS not supported in this device": "القراءة الصوتية غير مدعومة في هذا الجهاز",
|
||||
"Theme Color": "لون السمة",
|
||||
"Theme Mode": "وضع السمة",
|
||||
"Translate": "ترجمة",
|
||||
"Translated Text": "النص المترجم",
|
||||
"Unknown": "غير معروف",
|
||||
"Untitled": "بدون عنوان",
|
||||
"Updated:": "تم التحديث:",
|
||||
"User avatar": "صورة المستخدم",
|
||||
"Version {{version}}": "الإصدار {{version}}",
|
||||
"Vertical Direction": "الاتجاه العمودي",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "مرحبًا بكم في مكتبتكم. يمكنكم استيراد كتبكم هنا وقراءتها في أي وقت.",
|
||||
"Wikipedia": "ويكيبيديا",
|
||||
"Writing Mode": "وضع الكتابة",
|
||||
"Your Library": "المكتبة الخاصة بك",
|
||||
"TTS not supported for PDF": "القراءة الصوتية غير مدعومة لملفات PDF",
|
||||
"Override Book Font": "تجاوز خط الكتاب",
|
||||
"Vertical Margins (px)": "هوامش عمودية (بكسل)",
|
||||
"Horizontal Margins (%)": "هوامش أفقية (%)",
|
||||
"Apply to All Books": "تطبيق على جميع الكتب",
|
||||
"Apply to This Book": "تطبيق على هذا الكتاب",
|
||||
"Unable to fetch the translation. Try again later.": "تعذر جلب الترجمة. حاول مرة أخرى لاحقًا.",
|
||||
"Update Now!": "قم بالتحديث الآن!",
|
||||
"Update": "تحديث",
|
||||
"Check Update": "التحقق من التحديث",
|
||||
"Already the latest version": "الإصدار الحالي هو الإصدار الأحدث بالفعل",
|
||||
"Book Details": "تفاصيل الكتاب",
|
||||
"From Local File": "من ملف محلي",
|
||||
"TOC": "جدول المحتويات",
|
||||
"Book uploaded: {{title}}": "تم تحميل الكتاب: {{title}}",
|
||||
"Failed to upload book: {{title}}": "فشل تحميل الكتاب: {{title}}",
|
||||
"Book downloaded: {{title}}": "تم تنزيل الكتاب: {{title}}",
|
||||
"Failed to download book: {{title}}": "فشل تنزيل الكتاب: {{title}}",
|
||||
"Upload Book": "تحميل الكتاب",
|
||||
"Auto Upload Books to Cloud": "تحميل الكتب تلقائيًا إلى السحابة",
|
||||
"{{percentage}}% of Cloud Storage Used.": "تم استخدام {{percentage}}% من مساحة التخزين السحابية.",
|
||||
"Storage": "التخزين",
|
||||
"Book deleted: {{title}}": "تم حذف الكتاب: {{title}}",
|
||||
"Failed to delete book: {{title}}": "فشل حذف الكتاب: {{title}}",
|
||||
"Check Updates on Start": "التحقق من التحديثات عند البدء",
|
||||
"Insufficient storage quota": "حصة التخزين غير كافية",
|
||||
"Font Weight": "سمك الخط",
|
||||
"Line Spacing": "تباعد الأسطر",
|
||||
"Word Spacing": "تباعد الكلمات",
|
||||
"Letter Spacing": "تباعد الأحرف",
|
||||
"Text Indent": "مسافة بادئة للنص",
|
||||
"Paragraph Margin": "هامش الفقرة",
|
||||
"Override Book Layout": "تجاوز تخطيط الكتاب",
|
||||
"Add to Group": "إضافة إلى مجموعة",
|
||||
"Untitled Group": "مجموعة بدون عنوان",
|
||||
"Group Books": "تجميع الكتب",
|
||||
"Remove From Group": "إزالة من المجموعة",
|
||||
"Create New Group": "إنشاء مجموعة جديدة",
|
||||
"Deselect Book": "إلغاء تحديد الكتاب",
|
||||
"Download Book": "تنزيل الكتاب",
|
||||
"Deselect Group": "إلغاء تحديد المجموعة",
|
||||
"Select Group": "تحديد المجموعة",
|
||||
"Keep Screen Awake": "إبقاء الشاشة نشطة",
|
||||
"Email address": "البريد الإلكتروني",
|
||||
"Your Password": "كلمة المرور",
|
||||
"Your email address": "البريد الإلكتروني الخاص بك",
|
||||
"Your password": "كلمة المرور الخاصة بك",
|
||||
"Sign in": "تسجيل الدخول",
|
||||
"Signing in...": "يتم الآن تسجيل الدخول...",
|
||||
"Sign in with {{provider}}": "تسجيل الدخول باستخدام {{provider}}",
|
||||
"Already have an account? Sign in": "هل لديك حساب بالفعل؟ تسجيل الدخول",
|
||||
"Create a Password": "إنشاء كلمة مرور",
|
||||
"Sign up": "إنشاء حساب",
|
||||
"Signing up...": "يتم الآن إنشاء الحساب...",
|
||||
"Don’t have an account? Sign up": "ليس لديك حساب؟ قم بالتسجيل",
|
||||
"Check your email for the confirmation link": "تحقق من بريدك الإلكتروني للحصول على رابط التأكيد",
|
||||
"Signing in ...": "يتم الآن تسجيل الدخول ...",
|
||||
"Send a magic link email": "إرسال رابط سحري إلى بريدك الإلكتروني",
|
||||
"Check your email for the magic link": "تحقق من بريدك الإلكتروني للحصول على الرابط السحري",
|
||||
"Send reset password instructions": "أرسل تعليمات إعادة تعيين كلمة المرور",
|
||||
"Sending reset instructions ...": "يتم الآن إرسال تعليمات إعادة تعيين كلمة المرور ...",
|
||||
"Forgot your password?": "نسيت كلمة المرور؟",
|
||||
"Check your email for the password reset link": "تحقق من بريدك الإلكتروني للحصول على رابط إعادة تعيين كلمة المرور",
|
||||
"New Password": "كلمة المرور الجديدة",
|
||||
"Your new password": "أدخل كلمة المرور الجديدة",
|
||||
"Update password": "تغيير كلمة المرور",
|
||||
"Updating password ...": "يتم الآن تغيير كلمة المرور ...",
|
||||
"Your password has been updated": "تم تغيير كلمة المرور الخاصة بك بنجاح",
|
||||
"Phone number": "رقم الجوال",
|
||||
"Your phone number": "أدخل رقم الجوال",
|
||||
"Token": "الرمز",
|
||||
"Your OTP token": "رمز OTP الذي وصلك",
|
||||
"Verify token": "التحقق من الرمز",
|
||||
"Sign in with Google": "تسجيل الدخول عبر Google",
|
||||
"Sign in with Apple": "تسجيل الدخول عبر Apple",
|
||||
"Sign in with GitHub": "تسجيل الدخول عبر GitHub"
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.17": {
|
||||
"date": "2025-02-24",
|
||||
"notes": [
|
||||
"Add Arabic localization",
|
||||
"Add Dockerfile to selfhost",
|
||||
"Add safari-auth plugin for iOS OAuth",
|
||||
"UX enhancements for mobile platforms with haptics feedback and pull-down dismissal"
|
||||
]
|
||||
},
|
||||
"0.9.15": {
|
||||
"date": "2025-02-21",
|
||||
"notes": [
|
||||
|
||||
@@ -45,6 +45,8 @@ tauri-plugin-oauth = "2"
|
||||
tauri-plugin-opener = "2.2.2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2.2.3"
|
||||
tauri-plugin-safari-auth = { path = "./plugins/tauri-plugin-safari-auth" }
|
||||
[patch.crates-io]
|
||||
tauri = { path = "../../../packages/tauri/crates/tauri" }
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
"oauth:allow-cancel",
|
||||
"sign-in-with-apple:default",
|
||||
"opener:default",
|
||||
"haptics:allow-vibrate",
|
||||
"haptics:allow-impact-feedback",
|
||||
"haptics:allow-notification-feedback",
|
||||
"haptics:allow-selection-feedback",
|
||||
"safari-auth:default",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea/
|
||||
debug.log
|
||||
package-lock.json
|
||||
.vscode/settings.json
|
||||
yarn.lock
|
||||
|
||||
/.tauri
|
||||
/target
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
|
||||
dist-js
|
||||
dist
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "tauri-plugin-safari-auth"
|
||||
version = "0.1.0"
|
||||
authors = [ "chrox" ]
|
||||
description = "OAuth with ASWebAuthenticationSession on iOS"
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-safari-auth"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.2.4" }
|
||||
serde = "1.0"
|
||||
thiserror = "2"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.3", features = ["build"] }
|
||||
@@ -0,0 +1 @@
|
||||
# Tauri Plugin safari-auth
|
||||
@@ -0,0 +1,8 @@
|
||||
const COMMANDS: &[&str] = &["auth_with_safari"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,32 @@
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-safari-auth",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-safari-auth",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-safari-auth"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-safari-auth",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tauri Plugin safari-auth
|
||||
|
||||
A description of this package.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import AuthenticationServices
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
class RequestArgs: Decodable {
|
||||
let authUrl: String
|
||||
}
|
||||
|
||||
struct Response: Encodable {
|
||||
let redirectUrl: String
|
||||
}
|
||||
|
||||
class SafariAuthPlugin: Plugin {
|
||||
private var authSession: ASWebAuthenticationSession?
|
||||
|
||||
@objc public func auth_with_safari(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(RequestArgs.self)
|
||||
let authUrl = URL(string: args.authUrl)!
|
||||
|
||||
authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: "readest") {
|
||||
[weak self] callbackURL, error in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.error("Auth session error: \(error.localizedDescription)")
|
||||
invoke.reject(error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
if let callbackURL = callbackURL {
|
||||
Logger.info("Auth session callback URL: \(callbackURL.absoluteString)")
|
||||
strongSelf.authSession?.cancel()
|
||||
strongSelf.authSession = nil
|
||||
invoke.resolve(["redirectUrl": callbackURL.absoluteString])
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
authSession?.presentationContextProvider = self
|
||||
}
|
||||
|
||||
let started = authSession?.start() ?? false
|
||||
Logger.info("Auth session start result: \(started)")
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_safari_auth")
|
||||
func initPlugin() -> Plugin {
|
||||
return SafariAuthPlugin()
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension SafariAuthPlugin: ASWebAuthenticationPresentationContextProviding {
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return UIApplication.shared.windows.first ?? UIWindow()
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import SafariAuthPlugin
|
||||
|
||||
final class SafariAuthPluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = SafariAuthPlugin()
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-auth-with-safari"
|
||||
description = "Enables the auth_with_safari command without any pre-configured scope."
|
||||
commands.allow = ["auth_with_safari"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-auth-with-safari"
|
||||
description = "Denies the auth_with_safari command without any pre-configured scope."
|
||||
commands.deny = ["auth_with_safari"]
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
## Default Permission
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
- `allow-auth-with-safari`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`safari-auth:allow-auth-with-safari`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the auth_with_safari command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`safari-auth:deny-auth-with-safari`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the auth_with_safari command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,3 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-auth-with-safari"]
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"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.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"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.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the auth_with_safari command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-auth-with-safari"
|
||||
},
|
||||
{
|
||||
"description": "Denies the auth_with_safari command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-auth-with-safari"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use tauri::{command, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
use crate::Result;
|
||||
use crate::SafariAuthExt;
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn auth_with_safari<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
payload: SafariAuthRequest,
|
||||
) -> Result<SafariAuthResponse> {
|
||||
app.safari_auth().auth_with_safari(payload)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<SafariAuth<R>> {
|
||||
Ok(SafariAuth(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the safari-auth APIs.
|
||||
pub struct SafariAuth<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> SafariAuth<R> {
|
||||
pub fn auth_with_safari(
|
||||
&self,
|
||||
_payload: SafariAuthRequest,
|
||||
) -> crate::Result<SafariAuthResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unsupported platform for this plugin")]
|
||||
UnsupportedPlatformError,
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::SafariAuth;
|
||||
#[cfg(mobile)]
|
||||
use mobile::SafariAuth;
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the safari-auth APIs.
|
||||
pub trait SafariAuthExt<R: Runtime> {
|
||||
fn safari_auth(&self) -> &SafariAuth<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::SafariAuthExt<R> for T {
|
||||
fn safari_auth(&self) -> &SafariAuth<R> {
|
||||
self.state::<SafariAuth<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("safari-auth")
|
||||
.invoke_handler(tauri::generate_handler![commands::auth_with_safari])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let safari_auth = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let safari_auth = desktop::init(app, api)?;
|
||||
app.manage(safari_auth);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_safari_auth);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<SafariAuth<R>> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_safari_auth)?;
|
||||
Ok(SafariAuth(handle))
|
||||
}
|
||||
|
||||
/// Access to the safari-auth APIs.
|
||||
pub struct SafariAuth<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> SafariAuth<R> {
|
||||
pub fn auth_with_safari(
|
||||
&self,
|
||||
payload: SafariAuthRequest,
|
||||
) -> crate::Result<SafariAuthResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("auth_with_safari", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SafariAuthRequest {
|
||||
pub auth_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SafariAuthResponse {
|
||||
pub redirect_url: String,
|
||||
}
|
||||
@@ -9,7 +9,7 @@ extern crate objc;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod traffic_light_plugin;
|
||||
mod traffic_light;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
@@ -106,7 +106,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init());
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_safari_auth::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
@@ -127,11 +128,14 @@ pub fn run() {
|
||||
let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.plugin(traffic_light_plugin::init());
|
||||
let builder = builder.plugin(traffic_light::init());
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
let builder = builder.plugin(tauri_plugin_sign_in_with_apple::init());
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_haptics::init());
|
||||
|
||||
builder
|
||||
.setup(|#[allow(unused_variables)] app| {
|
||||
#[cfg(desktop)]
|
||||
|
||||
+2
-2
@@ -7,14 +7,14 @@ use tauri::{
|
||||
}; // 0.8
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 10.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 20.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 22.0;
|
||||
|
||||
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
||||
unsafe impl Send for UnsafeWindowHandle {}
|
||||
unsafe impl Sync for UnsafeWindowHandle {}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("traffic_light_positioner")
|
||||
Builder::new("traffic_light")
|
||||
.on_window_ready(|window| {
|
||||
#[cfg(target_os = "macos")]
|
||||
setup_traffic_light_positioner(window.clone());
|
||||
@@ -22,6 +22,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
|
||||
import { authWithSafari } from './utils/safariAuth';
|
||||
|
||||
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
|
||||
|
||||
@@ -66,12 +67,16 @@ export default function AuthPage() {
|
||||
const isOAuthServerRunning = useRef(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const getTauriRedirectTo = () => {
|
||||
return process.env.NODE_ENV === 'production' || appService?.isMobile
|
||||
? appService?.isMobile
|
||||
? WEB_AUTH_CALLBACK
|
||||
: DEEPLINK_CALLBACK
|
||||
: `http://localhost:${port}`; // only for development env on Desktop
|
||||
const getTauriRedirectTo = (isOAuth: boolean) => {
|
||||
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
|
||||
if (appService?.isIOSApp) {
|
||||
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
|
||||
} else if (appService?.isAndroidApp) {
|
||||
return WEB_AUTH_CALLBACK;
|
||||
}
|
||||
return DEEPLINK_CALLBACK;
|
||||
}
|
||||
return `http://localhost:${port}`; // only for development env on Desktop
|
||||
};
|
||||
|
||||
const tauriSignInApple = async () => {
|
||||
@@ -103,7 +108,7 @@ export default function AuthPage() {
|
||||
provider,
|
||||
options: {
|
||||
skipBrowserRedirect: true,
|
||||
redirectTo: getTauriRedirectTo(),
|
||||
redirectTo: getTauriRedirectTo(true),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,7 +116,16 @@ export default function AuthPage() {
|
||||
console.error('Authentication error:', error);
|
||||
return;
|
||||
}
|
||||
await openUrl(data.url);
|
||||
// Open the OAuth URL in a ASWebAuthenticationSession on iOS to comply with Apple's guidelines
|
||||
// for other platforms, open the OAuth URL in the default browser
|
||||
if (appService?.isIOSApp) {
|
||||
const res = await authWithSafari({ authUrl: data.url });
|
||||
if (res) {
|
||||
handleOAuthUrl(res.redirectUrl);
|
||||
}
|
||||
} else {
|
||||
await openUrl(data.url);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthUrl = async (url: string) => {
|
||||
@@ -318,16 +332,20 @@ export default function AuthPage() {
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='my-3 mt-6 w-64 border-t border-gray-200' />
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo()}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
{!appService?.isIOSApp && (
|
||||
<div>
|
||||
<hr className='my-3 mt-6 w-64 border-t border-gray-200' />
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo(false)}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface SafariAuthRequest {
|
||||
authUrl: string;
|
||||
}
|
||||
|
||||
export interface SafariAuthResponse {
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
export async function authWithSafari(request: SafariAuthRequest): Promise<SafariAuthResponse> {
|
||||
const result = await invoke<SafariAuthResponse>('plugin:safari-auth|auth_with_safari', {
|
||||
payload: request,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -219,7 +219,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
};
|
||||
|
||||
const { pressing, handlers } = useLongPress({
|
||||
onLongPress: () => {
|
||||
onLongPress: async () => {
|
||||
if (!isSelectMode) {
|
||||
handleSetSelectMode(true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
@@ -16,6 +16,7 @@ import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SettingsMenu from './SettingsMenu';
|
||||
import ImportMenu from './ImportMenu';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
isSelectMode: boolean;
|
||||
@@ -37,17 +38,9 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const iconSize20 = useResponsiveSize(20);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
onToggleSelectMode();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [onToggleSelectMode]);
|
||||
useShortcuts({
|
||||
onToggleSelectMode,
|
||||
});
|
||||
|
||||
const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible;
|
||||
const isInGroupView = !!searchParams?.get('group');
|
||||
@@ -56,7 +49,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 flex h-11 w-full items-center py-2 pr-4 sm:pr-6',
|
||||
'titlebar h-13 z-10 flex 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',
|
||||
)}
|
||||
@@ -75,7 +68,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<div className='relative flex h-7 w-full items-center'>
|
||||
<div className='relative flex h-9 w-full items-center sm:h-7'>
|
||||
<span className='absolute left-3 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
@@ -84,14 +77,14 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
placeholder={_('Search Books...')}
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
'input rounded-badge bg-base-300/50 h-7 w-full pl-10 pr-10',
|
||||
'input rounded-badge bg-base-300/50 h-9 w-full pl-10 pr-10 sm:h-7',
|
||||
'font-sans text-sm font-light',
|
||||
'border-none focus:outline-none focus:ring-0',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='absolute right-4 flex items-center space-x-2 text-gray-500 sm:space-x-4'>
|
||||
<span className='mx-2 h-6 w-[1px] bg-gray-400'></span>
|
||||
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
|
||||
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parseOpenWithFiles } from '@/helpers/cli';
|
||||
import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
@@ -26,6 +27,8 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
|
||||
import { useDemoBooks } from './hooks/useDemoBooks';
|
||||
import { useBooksSync } from './hooks/useBooksSync';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { tauriQuitApp } from '@/utils/window';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
@@ -33,7 +36,7 @@ import Spinner from '@/components/Spinner';
|
||||
import LibraryHeader from './components/LibraryHeader';
|
||||
import Bookshelf from './components/Bookshelf';
|
||||
import BookDetailModal from '@/components/BookDetailModal';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
|
||||
const LibraryPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -68,6 +71,14 @@ const LibraryPage = () => {
|
||||
usePullToRefresh(containerRef, pullLibrary);
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useShortcuts({
|
||||
onQuitApp: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriQuitApp();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-200');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -335,10 +346,16 @@ const LibraryPage = () => {
|
||||
};
|
||||
|
||||
const handleToggleSelectMode = () => {
|
||||
setIsSelectMode(!isSelectMode);
|
||||
if (!isSelectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode((pre) => !pre);
|
||||
};
|
||||
|
||||
const handleSetSelectMode = (selectMode: boolean) => {
|
||||
if (selectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode(selectMode);
|
||||
};
|
||||
|
||||
@@ -385,8 +402,9 @@ const LibraryPage = () => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
'scroll-container mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
|
||||
)}
|
||||
>
|
||||
<Bookshelf
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebar, TbLayoutSidebarFilled } from 'react-icons/tb';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
@@ -12,6 +13,7 @@ interface SidebarTogglerProps {
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, isSideBarVisible, setSideBarBookKey, toggleSideBar } = useSidebarStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
@@ -19,6 +21,7 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
setHoveredBookKey('');
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -357,6 +357,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (existingIndex !== -1) {
|
||||
views.forEach((view) => view?.addAnnotation(annotation, true));
|
||||
if (update) {
|
||||
annotation.id = annotations[existingIndex]!.id;
|
||||
annotations[existingIndex] = annotation;
|
||||
views.forEach((view) => view?.addAnnotation(annotation));
|
||||
} else {
|
||||
|
||||
@@ -37,8 +37,8 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
onHandleHighlight(true);
|
||||
};
|
||||
const handleSelectColor = (color: HighlightColor) => {
|
||||
const style = globalReadSettings.highlightStyle;
|
||||
globalReadSettings.highlightStyles[style] = color;
|
||||
globalReadSettings.highlightStyle = selectedStyle;
|
||||
globalReadSettings.highlightStyles[selectedStyle] = color;
|
||||
setSettings(settings);
|
||||
setSelectedColor(color);
|
||||
onHandleHighlight(true);
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
@@ -81,12 +81,6 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = 1 - e.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const handleSaveNote = (selection: TextSelection, note: string) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
@@ -132,7 +126,13 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
const onDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = 1 - data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(onDragMove);
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
@@ -175,7 +175,7 @@ const Notebook: React.FC = ({}) => {
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleDragStart}
|
||||
/>
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
|
||||
@@ -64,6 +64,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
/>
|
||||
}
|
||||
label={book.title}
|
||||
labelClass='max-w-36'
|
||||
onClick={() => handleParallelView(book.hash)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { BookSearchResult } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import BookCard from './BookCard';
|
||||
import useSidebar from '../../hooks/useSidebar';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import SearchBar from './SearchBar';
|
||||
import SearchResults from './SearchResults';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
@@ -33,6 +33,7 @@ const SideBar: React.FC<{
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const isMobile = window.innerWidth < 640;
|
||||
const {
|
||||
sideBarWidth,
|
||||
isSideBarPinned,
|
||||
@@ -42,7 +43,7 @@ const SideBar: React.FC<{
|
||||
handleSideBarTogglePin,
|
||||
} = useSidebar(
|
||||
settings.globalReadSettings.sideBarWidth,
|
||||
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
|
||||
isMobile ? false : settings.globalReadSettings.isSideBarPinned,
|
||||
);
|
||||
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
@@ -79,12 +80,52 @@ const SideBar: React.FC<{
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = e.clientX / window.innerWidth;
|
||||
const handleVerticalDragMove = (data: { clientY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const heightFraction = data.clientY / window.innerHeight;
|
||||
const newTop = Math.max(0.0, Math.min(1, heightFraction));
|
||||
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (sidebar && overlay) {
|
||||
sidebar.style.top = `${newTop * 100}%`;
|
||||
overlay.style.opacity = `${1 - heightFraction}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerticalDragEnd = (velocity: number) => {
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (!sidebar || !overlay) return;
|
||||
|
||||
if (velocity > 0.5) {
|
||||
sidebar.style.transition = `top ${0.15 / velocity}s ease-out`;
|
||||
sidebar.style.top = '100%';
|
||||
overlay.style.transition = `opacity ${0.15 / velocity}s ease-out`;
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => setSideBarVisible(false), 300);
|
||||
} else {
|
||||
sidebar.style.transition = 'top 0.3s ease-out';
|
||||
sidebar.style.top = '0%';
|
||||
overlay.style.transition = 'opacity 0.3s ease-out';
|
||||
overlay.style.opacity = '0.8';
|
||||
}
|
||||
};
|
||||
|
||||
const handleHorizontalDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, widthFraction));
|
||||
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
|
||||
const { handleDragStart: handleVerticalDragStart } = useDrag(
|
||||
handleVerticalDragMove,
|
||||
handleVerticalDragEnd,
|
||||
);
|
||||
const { handleDragStart: handleHorizontalDragStart } = useDrag(handleHorizontalDragMove);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisible(false);
|
||||
@@ -135,10 +176,27 @@ const SideBar: React.FC<{
|
||||
.sidebar-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
.sidebar-container.open {
|
||||
top: 0%;
|
||||
}
|
||||
.overlay {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex-shrink-0'>
|
||||
{isMobile && (
|
||||
<div
|
||||
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
|
||||
onMouseDown={handleVerticalDragStart}
|
||||
onTouchStart={handleVerticalDragStart}
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
<SidebarHeader
|
||||
isPinned={isSideBarPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
@@ -174,11 +232,14 @@ const SideBar: React.FC<{
|
||||
)}
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleHorizontalDragStart}
|
||||
></div>
|
||||
</div>
|
||||
{!isSideBarPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
<div
|
||||
className='overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { tauriQuitApp } from '@/utils/window';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
|
||||
|
||||
interface UseBookShortcutsProps {
|
||||
@@ -61,6 +61,22 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
getView(sideBarBookKey)?.history.back();
|
||||
};
|
||||
|
||||
const goHalfPageDown = () => {
|
||||
const view = getView(sideBarBookKey);
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
if (view && viewSettings && viewSettings.scrolled) {
|
||||
view.next(view.renderer.size / 2);
|
||||
}
|
||||
};
|
||||
|
||||
const goHalfPageUp = () => {
|
||||
const view = getView(sideBarBookKey);
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
if (view && viewSettings && viewSettings.scrolled) {
|
||||
view.prev(view.renderer.size / 2);
|
||||
}
|
||||
};
|
||||
|
||||
const goForward = () => {
|
||||
getView(sideBarBookKey)?.history.forward();
|
||||
};
|
||||
@@ -72,9 +88,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
const quitApp = async () => {
|
||||
// on web platform use browser's default shortcut to close the tab
|
||||
if (isTauriAppPlatform()) {
|
||||
await eventDispatcher.dispatch('quit-app');
|
||||
const { exit } = await import('@tauri-apps/plugin-process');
|
||||
await exit(0);
|
||||
await tauriQuitApp();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,6 +137,8 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onGoRight: goRight,
|
||||
onGoPrev: goPrev,
|
||||
onGoNext: goNext,
|
||||
onGoHalfPageDown: goHalfPageDown,
|
||||
onGoHalfPageUp: goHalfPageUp,
|
||||
onGoBack: goBack,
|
||||
onGoForward: goForward,
|
||||
onZoomIn: zoomIn,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useDragBar = (handleDragMove: (e: MouseEvent) => void) => {
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
handleDragMove(e);
|
||||
},
|
||||
[handleDragMove],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
},
|
||||
[handleMouseMove, handleMouseUp],
|
||||
);
|
||||
|
||||
return { handleMouseDown };
|
||||
};
|
||||
|
||||
export default useDragBar;
|
||||
@@ -1,8 +1,13 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
|
||||
const DISMISS_THRESHOLD = 100;
|
||||
const VELOCITY_THRESHOLD = 0.5;
|
||||
|
||||
interface DialogProps {
|
||||
id?: string;
|
||||
@@ -28,7 +33,10 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const [translateY, setTranslateY] = useState(0);
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
@@ -43,23 +51,67 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragMove = (data: { clientY: number; deltaY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const modal = document.querySelector('.modal-box') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
if (modal && overlay) {
|
||||
modal.style.transition = '';
|
||||
overlay.style.opacity = `${1 - data.clientY / window.innerHeight}`;
|
||||
}
|
||||
setTranslateY((prev) => Math.max(prev + data.deltaY, 0));
|
||||
};
|
||||
|
||||
const handleDragEnd = (velocity: number) => {
|
||||
const modal = document.querySelector('.modal-box') as HTMLElement;
|
||||
if (!modal) return;
|
||||
|
||||
if (translateY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
|
||||
modal.style.transition = `transform ${0.15 / velocity}s ease-out`;
|
||||
onClose();
|
||||
setTimeout(() => setTranslateY(0), 300);
|
||||
if (appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
} else {
|
||||
modal.style.transition = `transform 0.3s ease-out`;
|
||||
setTranslateY(0);
|
||||
if (appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(handleDragMove, handleDragEnd);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
id={id ?? 'dialog'}
|
||||
open={isOpen}
|
||||
className={clsx(
|
||||
'modal sm:min-w-90 z-50 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full',
|
||||
className,
|
||||
)}
|
||||
className={clsx('modal sm:min-w-90 z-50 h-full w-full sm:w-full', className)}
|
||||
>
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/30 sm:bg-black/20' />
|
||||
<div
|
||||
className={clsx(
|
||||
'modal-box settings-content flex flex-col rounded-none p-0 sm:rounded-2xl',
|
||||
'modal-box settings-content z-20 flex flex-col rounded-none rounded-tl-2xl rounded-tr-2xl p-0 sm:rounded-2xl',
|
||||
'h-full max-h-full w-full max-w-full sm:w-[65%] sm:max-w-[600px]',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)] sm:pt-0',
|
||||
boxClassName,
|
||||
)}
|
||||
style={{
|
||||
transform: `translateY(${translateY}px)`,
|
||||
}}
|
||||
>
|
||||
{window.innerWidth < 640 && (
|
||||
<div
|
||||
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
|
||||
onMouseDown={handleDragStart}
|
||||
onTouchStart={handleDragStart}
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
|
||||
{header ? (
|
||||
header
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ShortcutConfig {
|
||||
onToggleNotebook: string[];
|
||||
onToggleSearchBar: string[];
|
||||
onToggleScrollMode: string[];
|
||||
onToggleSelectMode: string[];
|
||||
onOpenFontLayoutSettings: string[];
|
||||
onReloadPage: string[];
|
||||
onQuitApp: string[];
|
||||
@@ -11,6 +12,8 @@ export interface ShortcutConfig {
|
||||
onGoRight: string[];
|
||||
onGoNext: string[];
|
||||
onGoPrev: string[];
|
||||
onGoHalfPageDown: string[];
|
||||
onGoHalfPageUp: string[];
|
||||
onGoBack: string[];
|
||||
onGoForward: string[];
|
||||
onZoomIn: string[];
|
||||
@@ -26,6 +29,7 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onToggleNotebook: ['n'],
|
||||
onToggleSearchBar: ['ctrl+f', 'cmd+f'],
|
||||
onToggleScrollMode: ['shift+j'],
|
||||
onToggleSelectMode: ['shift+s'],
|
||||
onOpenFontLayoutSettings: ['shift+f'],
|
||||
onReloadPage: ['shift+r'],
|
||||
onQuitApp: ['ctrl+q', 'cmd+q'],
|
||||
@@ -33,17 +37,20 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l', ' '],
|
||||
onGoNext: ['ArrowDown', 'j'],
|
||||
onGoPrev: ['ArrowUp', 'k'],
|
||||
onGoHalfPageDown: ['shift+ArrowDown', 'd'],
|
||||
onGoHalfPageUp: ['shift+ArrowUp', 'u'],
|
||||
onGoBack: ['shift+ArrowLeft', 'shift+h'],
|
||||
onGoForward: ['shift+ArrowRight', 'shift+l'],
|
||||
onZoomIn: ['ctrl+=', 'cmd+=', 'shift+='],
|
||||
onZoomOut: ['ctrl+-', 'cmd+-', 'shift+-'],
|
||||
onResetZoom: ['ctrl+0', 'cmd+0'],
|
||||
onSaveNote: ['ctrl+Enter'],
|
||||
onCloseNote: ['Escape']
|
||||
onCloseNote: ['Escape'],
|
||||
};
|
||||
|
||||
// Load shortcuts from localStorage or fallback to defaults
|
||||
export const loadShortcuts = (): ShortcutConfig => {
|
||||
if (typeof localStorage === 'undefined') return DEFAULT_SHORTCUTS;
|
||||
const customShortcuts = JSON.parse(localStorage.getItem('customShortcuts') || '{}');
|
||||
return {
|
||||
...DEFAULT_SHORTCUTS,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useDrag = (
|
||||
onDragMove: (data: { clientX: number; clientY: number; deltaX: number; deltaY: number }) => void,
|
||||
onDragEnd?: (velocity: number) => void,
|
||||
) => {
|
||||
const isDragging = useRef(false);
|
||||
const startX = useRef(0);
|
||||
const startY = useRef(0);
|
||||
const lastX = useRef(0);
|
||||
const lastY = useRef(0);
|
||||
const startTime = useRef(0);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
isDragging.current = true;
|
||||
|
||||
if ('touches' in e) {
|
||||
startY.current = e.touches[0]!.clientY;
|
||||
startX.current = e.touches[0]!.clientX;
|
||||
} else {
|
||||
startY.current = e.clientY;
|
||||
startX.current = e.clientX;
|
||||
}
|
||||
startTime.current = performance.now();
|
||||
|
||||
const handleMove = (event: MouseEvent | TouchEvent) => {
|
||||
if (isDragging.current) {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
let clientX = 0;
|
||||
let clientY = 0;
|
||||
|
||||
if ('touches' in event && event.touches.length > 0) {
|
||||
const currentTouch = event.touches[0]!;
|
||||
clientX = currentTouch.clientX;
|
||||
clientY = currentTouch.clientY;
|
||||
} else {
|
||||
const evt = event as MouseEvent;
|
||||
clientX = evt.clientX;
|
||||
clientY = evt.clientY;
|
||||
}
|
||||
deltaX = clientX - lastX.current;
|
||||
deltaY = clientY - lastY.current;
|
||||
lastX.current = clientX;
|
||||
lastY.current = clientY;
|
||||
|
||||
onDragMove({ clientX, clientY, deltaX, deltaY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnd = (event: MouseEvent | TouchEvent) => {
|
||||
isDragging.current = false;
|
||||
const endTime = performance.now();
|
||||
const deltaT = endTime - startTime.current;
|
||||
|
||||
const distanceY =
|
||||
'touches' in event
|
||||
? event.changedTouches[0]!.clientY - startY.current
|
||||
: event.clientY - startY.current;
|
||||
const velocity = distanceY / deltaT;
|
||||
|
||||
if (onDragEnd) {
|
||||
onDragEnd(velocity);
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', handleMove);
|
||||
window.removeEventListener('mouseup', handleEnd);
|
||||
window.removeEventListener('touchmove', handleMove);
|
||||
window.removeEventListener('touchend', handleEnd);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMove, { passive: true });
|
||||
window.addEventListener('mouseup', handleEnd);
|
||||
window.addEventListener('touchmove', handleMove, { passive: true });
|
||||
window.addEventListener('touchend', handleEnd);
|
||||
},
|
||||
[onDragMove, onDragEnd],
|
||||
);
|
||||
|
||||
return { handleDragStart };
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export const usePullToRefresh = (ref: React.RefObject<HTMLDivElement>, onTrigger
|
||||
const initialX = startEvent.touches[0]!.clientX;
|
||||
const initialY = startEvent.touches[0]!.clientY;
|
||||
|
||||
el.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
el.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||
el.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
function handleTouchMove(moveEvent: TouchEvent) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useScreenWakeLock = (lock: boolean) => {
|
||||
console.log('Wake lock acquired');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to acquire wake lock:', err);
|
||||
console.info('Failed to acquire wake lock:', err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { supabase } from '@/utils/supabase';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
|
||||
const DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
|
||||
const DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
|
||||
@@ -18,11 +19,22 @@ const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
const getDeepLAPIKey = (keys: string | undefined) => {
|
||||
const keyArray = keys?.split(',') ?? [];
|
||||
return keyArray.length ? keyArray[Math.floor(Math.random() * keyArray.length)] : '';
|
||||
};
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { user, token } = await getUserAndToken(req.headers['authorization']);
|
||||
const deeplApiUrl = user && token ? DEEPL_PRO_API : DEEPL_FREE_API;
|
||||
let deeplApiUrl = DEEPL_FREE_API;
|
||||
if (user && token) {
|
||||
const userPlan = await getUserPlan(token);
|
||||
if (userPlan !== 'free') deeplApiUrl = DEEPL_PRO_API;
|
||||
}
|
||||
const deeplAuthKey =
|
||||
user && token ? process.env['DEEPL_PRO_API_KEY'] : process.env['DEEPL_FREE_API_KEY'];
|
||||
deeplApiUrl === DEEPL_PRO_API
|
||||
? getDeepLAPIKey(process.env['DEEPL_PRO_API_KEYS'])
|
||||
: getDeepLAPIKey(process.env['DEEPL_FREE_API_KEYS']);
|
||||
|
||||
await runMiddleware(req, res, corsAllMethods);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export abstract class BaseAppService implements AppService {
|
||||
abstract hasContextMenu: boolean;
|
||||
abstract hasRoundedWindow: boolean;
|
||||
abstract hasSafeAreaInset: boolean;
|
||||
abstract hasHaptics: boolean;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
|
||||
@@ -126,6 +126,7 @@ export class NativeAppService extends BaseAppService {
|
||||
hasContextMenu = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -188,6 +188,7 @@ export class WebAppService extends BaseAppService {
|
||||
hasContextMenu = false;
|
||||
hasRoundedWindow = false;
|
||||
hasSafeAreaInset = isPWA();
|
||||
hasHaptics = false;
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -204,6 +204,7 @@ foliate-view {
|
||||
.scroll-container {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.scroll-container.hidden-scrollbar {
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface AppService {
|
||||
hasContextMenu: boolean;
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
hasHaptics: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
isAndroidApp: boolean;
|
||||
|
||||
@@ -36,8 +36,8 @@ export interface FoliateView extends HTMLElement {
|
||||
};
|
||||
renderer: {
|
||||
scrolled?: boolean;
|
||||
size: number;
|
||||
viewSize: number;
|
||||
size: number; // current page height
|
||||
viewSize: number; // whole document view height
|
||||
start: number;
|
||||
end: number;
|
||||
setAttribute: (name: string, value: string | number) => void;
|
||||
|
||||
@@ -10,6 +10,11 @@ interface Token {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
export const getUserPlan = async (token: string): Promise<UserPlan> => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
return data['plan'] || 'free';
|
||||
};
|
||||
|
||||
export const getStoragePlanData = (token: string) => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
const plan = data['plan'] || 'free';
|
||||
|
||||
@@ -16,6 +16,7 @@ const getFontStyles = (
|
||||
monospace: string,
|
||||
defaultFont: string,
|
||||
fontSize: number,
|
||||
minFontSize: number,
|
||||
fontWeight: number,
|
||||
overrideFont: boolean,
|
||||
) => {
|
||||
@@ -37,6 +38,27 @@ const getFontStyles = (
|
||||
font-size: ${fontSize}px !important;
|
||||
font-weight: ${fontWeight};
|
||||
}
|
||||
font[size="1"] {
|
||||
font-size: ${minFontSize}px;
|
||||
}
|
||||
font[size="2"] {
|
||||
font-size: ${minFontSize * 1.5}px;
|
||||
}
|
||||
font[size="3"] {
|
||||
font-size: ${fontSize}px;
|
||||
}
|
||||
font[size="4"] {
|
||||
font-size: ${fontSize * 1.2}px;
|
||||
}
|
||||
font[size="5"] {
|
||||
font-size: ${fontSize * 1.5}px;
|
||||
}
|
||||
font[size="6"] {
|
||||
font-size: ${fontSize * 2}px;
|
||||
}
|
||||
font[size="7"] {
|
||||
font-size: ${fontSize * 3}px;
|
||||
}
|
||||
body * {
|
||||
font-family: revert ${overrideFont ? '!important' : ''};
|
||||
font-family: inherit;
|
||||
@@ -255,6 +277,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
|
||||
viewSettings.monospaceFont!,
|
||||
viewSettings.defaultFont!,
|
||||
viewSettings.defaultFontSize! * fontScale,
|
||||
viewSettings.minimumFontSize!,
|
||||
viewSettings.fontWeight!,
|
||||
viewSettings.overrideFont!,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { eventDispatcher } from './event';
|
||||
|
||||
export const tauriGetWindowLogicalPosition = async () => {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const currentWindow = getCurrentWindow();
|
||||
@@ -41,3 +43,9 @@ export const tauriHandleOnWindowFocus = async (callback: () => void) => {
|
||||
await callback();
|
||||
});
|
||||
};
|
||||
|
||||
export const tauriQuitApp = async () => {
|
||||
await eventDispatcher.dispatch('quit-app');
|
||||
const { exit } = await import('@tauri-apps/plugin-process');
|
||||
await exit(0);
|
||||
};
|
||||
|
||||
Generated
+10
@@ -68,6 +68,9 @@ importers:
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@tauri-apps/plugin-haptics':
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
@@ -1735,6 +1738,9 @@ packages:
|
||||
'@tauri-apps/plugin-fs@2.2.0':
|
||||
resolution: {integrity: sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==}
|
||||
|
||||
'@tauri-apps/plugin-haptics@2.2.3':
|
||||
resolution: {integrity: sha512-tHWAOR0TSOuWIdJ4Fh/4z+L8CvSfHdg5i7XfCqjErPu63PMf+2n856VIGcC6fjF29hf/vq+BiGApWt38n66Gvg==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.3.0':
|
||||
resolution: {integrity: sha512-pigTvz+zzAqbIhCzRiR1GE98Jw7A03j2V+Eiexr9thBI8VfMiwFQMcbgON51xlwnVaI72LdbYKNajU84im8tlg==}
|
||||
|
||||
@@ -6636,6 +6642,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.2.0
|
||||
|
||||
'@tauri-apps/plugin-haptics@2.2.3':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.2.0
|
||||
|
||||
'@tauri-apps/plugin-http@2.3.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.2.0
|
||||
|
||||
Reference in New Issue
Block a user