forked from akai/readest
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 409c2f62be | |||
| 82922c5053 | |||
| 9c5bcb13e2 | |||
| 1d87dcdb7f | |||
| c7ba44a91e | |||
| 19b79c5b35 | |||
| 45216763e3 | |||
| 123293dc0e | |||
| 78e61060d2 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.90",
|
||||
"version": "0.9.91",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
|
||||
|
||||
@@ -651,5 +651,16 @@
|
||||
"Purchase Successful!": "تمت عملية الشراء بنجاح!",
|
||||
"lifetime": "مدى الحياة",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "شكرًا لشرائك! تم معالجة الدفع بنجاح.",
|
||||
"Order ID:": "معرف الطلب:"
|
||||
"Order ID:": "معرف الطلب:",
|
||||
"TTS Highlighting": "تمييز تحويل النص إلى كلام",
|
||||
"Style": "النمط",
|
||||
"Underline": "تسطير",
|
||||
"Strikethrough": "يتوسطه خط",
|
||||
"Squiggly": "متعرج",
|
||||
"Outline": "مخطط",
|
||||
"Save Current Color": "حفظ اللون الحالي",
|
||||
"Quick Colors": "ألوان سريعة",
|
||||
"Highlighter": "محدد النص",
|
||||
"Save Book Cover": "حفظ غلاف الكتاب",
|
||||
"Auto-save last book cover": "حفظ غلاف الكتاب الأخير تلقائيًا"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "ক্রয় সফল!",
|
||||
"lifetime": "জীবনকাল",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়া করা হয়েছে।",
|
||||
"Order ID:": "অর্ডার আইডি:"
|
||||
"Order ID:": "অর্ডার আইডি:",
|
||||
"TTS Highlighting": "টিটিএস হাইলাইটিং",
|
||||
"Style": "শৈলী",
|
||||
"Underline": "আন্ডারলাইন",
|
||||
"Strikethrough": "কাটা লেখা",
|
||||
"Squiggly": "ঢেউখেলানো দাগ",
|
||||
"Outline": "বাহিরের রেখা",
|
||||
"Save Current Color": "বর্তমান রঙ সংরক্ষণ করুন",
|
||||
"Quick Colors": "দ্রুত রংসমূহ",
|
||||
"Highlighter": "হাইলাইটার",
|
||||
"Save Book Cover": "বইয়ের মলাট সংরক্ষণ করুন",
|
||||
"Auto-save last book cover": "শেষ বইয়ের মলাট স্বয়ংক্রিয়ভাবে সংরক্ষণ করুন"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "ক্রয় সফল!",
|
||||
"lifetime": "জীবনকাল",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়াকৃত হয়েছে।",
|
||||
"Order ID:": "অর্ডার আইডি:"
|
||||
"Order ID:": "অর্ডার আইডি:",
|
||||
"TTS Highlighting": "ཡིག་འབྲུ་སྦྱོར་བའི་གསལ་བཤད།",
|
||||
"Style": "བཟོ་ལྟ།",
|
||||
"Underline": "འོག་ཐིག",
|
||||
"Strikethrough": "འཐེན་ཐིག",
|
||||
"Squiggly": "འཁྱོག་ཐིག",
|
||||
"Outline": "མཐའ་ཐིག",
|
||||
"Save Current Color": "མིག་སྔའི་ཚོན་གཏན་འཁེལ་བྱེད།",
|
||||
"Quick Colors": "མྱུར་ཚོན།",
|
||||
"Highlighter": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
|
||||
"Save Book Cover": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
|
||||
"Auto-save last book cover": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "Kauf erfolgreich!",
|
||||
"lifetime": "lebenslang",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Danke für Ihren Kauf! Ihre Zahlung wurde erfolgreich verarbeitet.",
|
||||
"Order ID:": "Bestell-ID:"
|
||||
"Order ID:": "Bestell-ID:",
|
||||
"TTS Highlighting": "TTS-Hervorhebung",
|
||||
"Style": "Stil",
|
||||
"Underline": "Unterstreichen",
|
||||
"Strikethrough": "Durchgestrichen",
|
||||
"Squiggly": "Wellig",
|
||||
"Outline": "Umriss",
|
||||
"Save Current Color": "Aktuelle Farbe speichern",
|
||||
"Quick Colors": "Schnellfarben",
|
||||
"Highlighter": "Textmarker",
|
||||
"Save Book Cover": "Buchcover speichern",
|
||||
"Auto-save last book cover": "Letztes Buchcover automatisch speichern"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "Η αγορά ήταν επιτυχής!",
|
||||
"lifetime": "διαρκής",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Σας ευχαριστούμε για την αγορά σας! Η πληρωμή σας έχει επεξεργαστεί με επιτυχία.",
|
||||
"Order ID:": "Αριθμός παραγγελίας:"
|
||||
"Order ID:": "Αριθμός παραγγελίας:",
|
||||
"TTS Highlighting": "Επισήμανση TTS",
|
||||
"Style": "Στυλ",
|
||||
"Underline": "Υπογράμμιση",
|
||||
"Strikethrough": "Διαγραμμένο",
|
||||
"Squiggly": "Κυματιστό",
|
||||
"Outline": "Περίγραμμα",
|
||||
"Save Current Color": "Αποθήκευση τρέχουσας χρώματος",
|
||||
"Quick Colors": "Γρήγορα χρώματα",
|
||||
"Highlighter": "Υπογραμμιστής",
|
||||
"Save Book Cover": "Αποθήκευση εξωφύλλου βιβλίου",
|
||||
"Auto-save last book cover": "Αυτόματη αποθήκευση τελευταίου εξωφύλλου βιβλίου"
|
||||
}
|
||||
|
||||
@@ -639,5 +639,16 @@
|
||||
"Purchase Successful!": "¡Compra Exitosa!",
|
||||
"lifetime": "de por vida",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "¡Gracias por tu compra! Tu pago se ha procesado correctamente.",
|
||||
"Order ID:": "ID de pedido:"
|
||||
"Order ID:": "ID de pedido:",
|
||||
"TTS Highlighting": "TTS Resaltado",
|
||||
"Style": "Estilo",
|
||||
"Underline": "Subrayado",
|
||||
"Strikethrough": "Tachado",
|
||||
"Squiggly": "Ondulado",
|
||||
"Outline": "Contorno",
|
||||
"Save Current Color": "Guardar Color Actual",
|
||||
"Quick Colors": "Colores Rápidos",
|
||||
"Highlighter": "Resaltador",
|
||||
"Save Book Cover": "Guardar Portada del Libro",
|
||||
"Auto-save last book cover": "Guardar automáticamente la última portada del libro"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "خرید با موفقیت انجام شد!",
|
||||
"lifetime": "مادامالعمر",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "از خرید شما متشکریم! پرداخت شما با موفقیت انجام شد.",
|
||||
"Order ID:": "شناسه سفارش:"
|
||||
"Order ID:": "شناسه سفارش:",
|
||||
"TTS Highlighting": "هایلایت TTS",
|
||||
"Style": "سبک",
|
||||
"Underline": "خط زیرین",
|
||||
"Strikethrough": "خطخورده",
|
||||
"Squiggly": "خطموجدار",
|
||||
"Outline": "خطکشی",
|
||||
"Save Current Color": "ذخیره رنگ فعلی",
|
||||
"Quick Colors": "رنگهای سریع",
|
||||
"Highlighter": "ماژیک هایلایت",
|
||||
"Save Book Cover": "ذخیره جلد کتاب",
|
||||
"Auto-save last book cover": "ذخیره خودکار آخرین جلد کتاب"
|
||||
}
|
||||
|
||||
@@ -639,5 +639,16 @@
|
||||
"Purchase Successful!": "Achat Réussi!",
|
||||
"lifetime": "à vie",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Merci pour votre achat ! Votre paiement a été traité avec succès.",
|
||||
"Order ID:": "ID de commande :"
|
||||
"Order ID:": "ID de commande :",
|
||||
"TTS Highlighting": "Surbrillance TTS",
|
||||
"Style": "Style",
|
||||
"Underline": "Souligner",
|
||||
"Strikethrough": "Barré",
|
||||
"Squiggly": "Ondulé",
|
||||
"Outline": "Contour",
|
||||
"Save Current Color": "Enregistrer la Couleur Actuelle",
|
||||
"Quick Colors": "Couleurs Rapides",
|
||||
"Highlighter": "Surligneur",
|
||||
"Save Book Cover": "Enregistrer la Couverture du Livre",
|
||||
"Auto-save last book cover": "Enregistrer automatiquement la dernière couverture du livre"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "खरीदारी सफल!",
|
||||
"lifetime": "जीवनकाल",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "आपकी खरीदारी के लिए धन्यवाद! आपका भुगतान सफलतापूर्वक संसाधित हो गया है।",
|
||||
"Order ID:": "ऑर्डर आईडी:"
|
||||
"Order ID:": "ऑर्डर आईडी:",
|
||||
"TTS Highlighting": "TTS हाइलाइटिंग",
|
||||
"Style": "शैली",
|
||||
"Underline": "रेखांकित",
|
||||
"Strikethrough": "रेखांकित करें",
|
||||
"Squiggly": "स्क्विगली",
|
||||
"Outline": "आउटलाइन",
|
||||
"Save Current Color": "वर्तमान रंग सहेजें",
|
||||
"Quick Colors": "त्वरित रंग",
|
||||
"Highlighter": "हाइलाइटर",
|
||||
"Save Book Cover": "बुक कवर सहेजें",
|
||||
"Auto-save last book cover": "अंतिम पुस्तक कवर को स्वचालित रूप से सहेजें"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "Pembelian Berhasil!",
|
||||
"lifetime": "seumur hidup",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Terima kasih atas pembelian Anda! Pembayaran Anda telah berhasil diproses.",
|
||||
"Order ID:": "ID Pesanan:"
|
||||
"Order ID:": "ID Pesanan:",
|
||||
"TTS Highlighting": "TTS Menyorot",
|
||||
"Style": "Gaya",
|
||||
"Underline": "Garisan Bawah",
|
||||
"Strikethrough": "Garis Tengah",
|
||||
"Squiggly": "Bergelombang",
|
||||
"Outline": "Garis Besar",
|
||||
"Save Current Color": "Simpan Warna Saat Ini",
|
||||
"Quick Colors": "Warna Cepat",
|
||||
"Highlighter": "Penyorot",
|
||||
"Save Book Cover": "Simpan Sampul Buku",
|
||||
"Auto-save last book cover": "Simpan otomatis sampul buku terakhir"
|
||||
}
|
||||
|
||||
@@ -639,5 +639,16 @@
|
||||
"Purchase Successful!": "Acquisto Riuscito!",
|
||||
"lifetime": "a vita",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Grazie per il tuo acquisto! Il pagamento è stato elaborato con successo.",
|
||||
"Order ID:": "ID Ordine:"
|
||||
"Order ID:": "ID Ordine:",
|
||||
"TTS Highlighting": "Evidenziazione TTS",
|
||||
"Style": "Stile",
|
||||
"Underline": "Sottolineato",
|
||||
"Strikethrough": "Barrato",
|
||||
"Squiggly": "Ondulato",
|
||||
"Outline": "Contorno",
|
||||
"Save Current Color": "Salva Colore Corrente",
|
||||
"Quick Colors": "Colori Veloci",
|
||||
"Highlighter": "Evidenziatore",
|
||||
"Save Book Cover": "Salva Copertina del Libro",
|
||||
"Auto-save last book cover": "Salva automaticamente l'ultima copertina del libro"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "購入成功!",
|
||||
"lifetime": "ライフタイム",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "ご購入ありがとうございます!お支払いが正常に処理されました。",
|
||||
"Order ID:": "購入ID:"
|
||||
"Order ID:": "購入ID:",
|
||||
"TTS Highlighting": "TTSハイライト",
|
||||
"Style": "スタイル",
|
||||
"Underline": "下線",
|
||||
"Strikethrough": "取り消し線",
|
||||
"Squiggly": "波線",
|
||||
"Outline": "アウトライン",
|
||||
"Save Current Color": "現在の色を保存",
|
||||
"Quick Colors": "クイックカラー",
|
||||
"Highlighter": "蛍光ペン",
|
||||
"Save Book Cover": "本のカバーを保存",
|
||||
"Auto-save last book cover": "最後の本のカバーを自動保存"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "구매 성공!",
|
||||
"lifetime": "평생",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "구매해주셔서 감사합니다! 결제가 성공적으로 처리되었습니다.",
|
||||
"Order ID:": "주문 ID:"
|
||||
"Order ID:": "주문 ID:",
|
||||
"TTS Highlighting": "TTS 하이라이팅",
|
||||
"Style": "스타일",
|
||||
"Underline": "밑줄",
|
||||
"Strikethrough": "취소선",
|
||||
"Squiggly": "물결선",
|
||||
"Outline": "윤곽선",
|
||||
"Save Current Color": "현재 색상 저장",
|
||||
"Quick Colors": "빠른 색상",
|
||||
"Highlighter": "형광펜",
|
||||
"Save Book Cover": "책 표지 저장",
|
||||
"Auto-save last book cover": "마지막 책 표지 자동 저장"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "Aankoop Succesvol!",
|
||||
"lifetime": "levenslang",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Bedankt voor je aankoop! Je betaling is succesvol verwerkt.",
|
||||
"Order ID:": "Bestel-ID:"
|
||||
"Order ID:": "Bestel-ID:",
|
||||
"TTS Highlighting": "TTS-markering",
|
||||
"Style": "Stijl",
|
||||
"Underline": "Onderstrepen",
|
||||
"Strikethrough": "Doorhalen",
|
||||
"Squiggly": "Kronkelig",
|
||||
"Outline": "Omtrek",
|
||||
"Save Current Color": "Huidige kleur opslaan",
|
||||
"Quick Colors": "Snelle kleuren",
|
||||
"Highlighter": "Marker",
|
||||
"Save Book Cover": "Opslaan boekomslag",
|
||||
"Auto-save last book cover": "Automatisch opslaan laatste boekomslag"
|
||||
}
|
||||
|
||||
@@ -643,5 +643,16 @@
|
||||
"Purchase Successful!": "Zakup Udany!",
|
||||
"lifetime": "dożywotnio",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Dziękujemy za zakup! Twoja płatność została pomyślnie przetworzona.",
|
||||
"Order ID:": "ID zamówienia:"
|
||||
"Order ID:": "ID zamówienia:",
|
||||
"TTS Highlighting": "Wyróżnianie TTS",
|
||||
"Style": "Styl",
|
||||
"Underline": "Podkreślenie",
|
||||
"Strikethrough": "Przekreślenie",
|
||||
"Squiggly": "Falista linia",
|
||||
"Outline": "Obrys",
|
||||
"Save Current Color": "Zapisz bieżący kolor",
|
||||
"Quick Colors": "Szybkie kolory",
|
||||
"Highlighter": "Zakreślacz",
|
||||
"Save Book Cover": "Zapisz okładkę książki",
|
||||
"Auto-save last book cover": "Automatyczne zapisywanie ostatniej okładki książki"
|
||||
}
|
||||
|
||||
@@ -639,5 +639,16 @@
|
||||
"Purchase Successful!": "Compra Bem-Sucedida!",
|
||||
"lifetime": "vitalício",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Obrigado pela sua compra! Seu pagamento foi processado com sucesso.",
|
||||
"Order ID:": "ID do Pedido:"
|
||||
"Order ID:": "ID do Pedido:",
|
||||
"TTS Highlighting": "Destaque TTS",
|
||||
"Style": "Estilo",
|
||||
"Underline": "Sublinhado",
|
||||
"Strikethrough": "Tachado",
|
||||
"Squiggly": "Ondulado",
|
||||
"Outline": "Contorno",
|
||||
"Save Current Color": "Salvar cor atual",
|
||||
"Quick Colors": "Cores rápidas",
|
||||
"Highlighter": "Marcador",
|
||||
"Save Book Cover": "Salvar Capa do Livro",
|
||||
"Auto-save last book cover": "Salvar automaticamente a última capa do livro"
|
||||
}
|
||||
|
||||
@@ -643,5 +643,16 @@
|
||||
"Purchase Successful!": "Покупка Успешна!",
|
||||
"lifetime": "пожизненный",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Спасибо за вашу покупку! Ваш платеж был успешно обработан.",
|
||||
"Order ID:": "ID заказа:"
|
||||
"Order ID:": "ID заказа:",
|
||||
"TTS Highlighting": "Выделение TTS",
|
||||
"Style": "Стиль",
|
||||
"Underline": "Подчёркивание",
|
||||
"Strikethrough": "Зачёркивание",
|
||||
"Squiggly": "Волнистая линия",
|
||||
"Outline": "Контур",
|
||||
"Save Current Color": "Сохранить текущий цвет",
|
||||
"Quick Colors": "Быстрые цвета",
|
||||
"Highlighter": "Маркер",
|
||||
"Save Book Cover": "Сохранить обложку книги",
|
||||
"Auto-save last book cover": "Автоматически сохранять последнюю обложку книги"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "මිලදී ගැනීම සාර්ථකයි!",
|
||||
"lifetime": "පොදුකාලීන",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "ඔබේ මිලදී ගැනීමට ස්තූතියි! ඔබේ ගෙවීම සාර්ථකව සැකසිණි.",
|
||||
"Order ID:": "ඇණවුම් හැඳුනුම්පත:"
|
||||
"Order ID:": "ඇණවුම් හැඳුනුම්පත:",
|
||||
"TTS Highlighting": "TTS උද්දීපනය",
|
||||
"Style": "රූපය",
|
||||
"Underline": "ඇටය",
|
||||
"Strikethrough": "ඇඳුම",
|
||||
"Squiggly": "වැලි රේඛාව",
|
||||
"Outline": "පරිසීමාව",
|
||||
"Save Current Color": "දැන් ඇති වර්ණය සුරකින්න",
|
||||
"Quick Colors": "ක්ෂණික වර්ණ",
|
||||
"Highlighter": "ඉස්මතු කරන්නා",
|
||||
"Save Book Cover": "පොත් ආවරණය සුරකින්න",
|
||||
"Auto-save last book cover": "අවසන් පොත් ආවරණය ස්වයංක්රීයව සුරකින්න"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "Köp lyckades!",
|
||||
"lifetime": "livstid",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Tack för ditt köp! Din betalning har behandlats.",
|
||||
"Order ID:": "Order-ID:"
|
||||
"Order ID:": "Order-ID:",
|
||||
"TTS Highlighting": "TTS-markering",
|
||||
"Style": "Stil",
|
||||
"Underline": "Understrykning",
|
||||
"Strikethrough": "Genomstrykning",
|
||||
"Squiggly": "Vågig linje",
|
||||
"Outline": "Kontur",
|
||||
"Save Current Color": "Spara aktuell färg",
|
||||
"Quick Colors": "Snabba färger",
|
||||
"Highlighter": "Markeringspenna",
|
||||
"Save Book Cover": "Spara bokomslag",
|
||||
"Auto-save last book cover": "Automatiskt spara senaste bokomslag"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "கொள்முதல் வெற்றிகரமாக முடிந்தது!",
|
||||
"lifetime": "ஆயுள்திருத்தம்",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "உங்கள் வாங்குதலுக்கு நன்றி! உங்கள் கட்டணம் வெற்றிகரமாக செயல்படுத்தப்பட்டது.",
|
||||
"Order ID:": "ஆர்டர் ஐடி:"
|
||||
"Order ID:": "ஆர்டர் ஐடி:",
|
||||
"TTS Highlighting": "TTS சிறப்பித்தல்",
|
||||
"Style": "பாணி",
|
||||
"Underline": "அடிக்கோடு",
|
||||
"Strikethrough": "குறிக்கோடு",
|
||||
"Squiggly": "சுருள் கோடு",
|
||||
"Outline": "வட்டம்",
|
||||
"Save Current Color": "தற்போதைய நிறத்தை சேமிக்கவும்",
|
||||
"Quick Colors": "விரைவு நிறங்கள்",
|
||||
"Highlighter": "ஹைலைட்டர்",
|
||||
"Save Book Cover": "புத்தகக் கோப்பைச் சேமிக்கவும்",
|
||||
"Auto-save last book cover": "அවසන් புத்தகக் கோப்பு காலியாக உள்ளது"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "การซื้อสำเร็จ!",
|
||||
"lifetime": "ตลอดชีพ",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "ขอบคุณสำหรับการซื้อ! การชำระเงินของคุณได้รับการประมวลผลเรียบร้อยแล้ว",
|
||||
"Order ID:": "รหัสคำสั่งซื้อ:"
|
||||
"Order ID:": "รหัสคำสั่งซื้อ:",
|
||||
"TTS Highlighting": "การไฮไลต์ TTS",
|
||||
"Style": "สไตล์",
|
||||
"Underline": "ขีดเส้นใต้",
|
||||
"Strikethrough": "ขีดฆ่า",
|
||||
"Squiggly": "เส้นหยัก",
|
||||
"Outline": "เส้นขอบ",
|
||||
"Save Current Color": "บันทึกสีปัจจุบัน",
|
||||
"Quick Colors": "สีด่วน",
|
||||
"Highlighter": "ปากกาเน้นข้อความ",
|
||||
"Save Book Cover": "บันทึกปกหนังสือ",
|
||||
"Auto-save last book cover": "บันทึกปกหนังสือสุดท้ายโดยอัตโนมัติ"
|
||||
}
|
||||
|
||||
@@ -635,5 +635,16 @@
|
||||
"Purchase Successful!": "Satın Alma Başarılı!",
|
||||
"lifetime": "ömür boyu",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Satın alma işleminiz için teşekkürler! Ödemeniz başarıyla işlendi.",
|
||||
"Order ID:": "Sipariş Kimliği:"
|
||||
"Order ID:": "Sipariş Kimliği:",
|
||||
"TTS Highlighting": "TTS Vurgulama",
|
||||
"Style": "Stil",
|
||||
"Underline": "Altı Çizili",
|
||||
"Strikethrough": "Üstü Çizili",
|
||||
"Squiggly": "Dalgalı Çizgi",
|
||||
"Outline": "Kontur",
|
||||
"Save Current Color": "Mevcut Rengi Kaydet",
|
||||
"Quick Colors": "Hızlı Renkler",
|
||||
"Highlighter": "Vurgulayıcı",
|
||||
"Save Book Cover": "Kitap Kapağını Kaydet",
|
||||
"Auto-save last book cover": "Son kitap kapağını otomatik olarak kaydet"
|
||||
}
|
||||
|
||||
@@ -643,5 +643,16 @@
|
||||
"Purchase Successful!": "Покупка успішна!",
|
||||
"lifetime": "довічний",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Дякуємо за вашу покупку! Ваш платіж успішно оброблено.",
|
||||
"Order ID:": "ID замовлення:"
|
||||
"Order ID:": "ID замовлення:",
|
||||
"TTS Highlighting": "Підсвічування TTS",
|
||||
"Style": "Стиль",
|
||||
"Underline": "Підкреслення",
|
||||
"Strikethrough": "Закреслення",
|
||||
"Squiggly": "Хвиляста лінія",
|
||||
"Outline": "Контур",
|
||||
"Save Current Color": "Зберегти поточний колір",
|
||||
"Quick Colors": "Швидкі кольори",
|
||||
"Highlighter": "Маркер",
|
||||
"Save Book Cover": "Зберегти обкладинку книги",
|
||||
"Auto-save last book cover": "Автоматично зберегти останню обкладинку книги"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "Mua hàng thành công!",
|
||||
"lifetime": "trọn đời",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "Cảm ơn bạn đã mua hàng! Thanh toán của bạn đã được xử lý thành công.",
|
||||
"Order ID:": "Mã đơn hàng:"
|
||||
"Order ID:": "Mã đơn hàng:",
|
||||
"TTS Highlighting": "Đánh dấu TTS",
|
||||
"Style": "Kiểu",
|
||||
"Underline": "Gạch chân",
|
||||
"Strikethrough": "Gạch ngang",
|
||||
"Squiggly": "Gạch ngoằn ngoèo",
|
||||
"Outline": "Đường viền",
|
||||
"Save Current Color": "Lưu màu hiện tại",
|
||||
"Quick Colors": "Màu nhanh",
|
||||
"Highlighter": "Bút đánh dấu",
|
||||
"Save Book Cover": "Lưu bìa sách",
|
||||
"Auto-save last book cover": "Tự động lưu bìa sách cuối cùng"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "购买成功!",
|
||||
"lifetime": "永久",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "感谢您的购买!您的付款已成功处理。",
|
||||
"Order ID:": "订单号:"
|
||||
"Order ID:": "订单号:",
|
||||
"TTS Highlighting": "TTS 高亮",
|
||||
"Style": "样式",
|
||||
"Underline": "下划线",
|
||||
"Strikethrough": "删除线",
|
||||
"Squiggly": "波浪线",
|
||||
"Outline": "轮廓",
|
||||
"Save Current Color": "保存当前颜色",
|
||||
"Quick Colors": "快速颜色",
|
||||
"Highlighter": "荧光笔",
|
||||
"Save Book Cover": "保存书籍封面",
|
||||
"Auto-save last book cover": "自动保存最后一本书的封面"
|
||||
}
|
||||
|
||||
@@ -631,5 +631,16 @@
|
||||
"Purchase Successful!": "購買成功!",
|
||||
"lifetime": "永久",
|
||||
"Thank you for your purchase! Your payment has been processed successfully.": "感謝您的購買!您的付款已成功處理。",
|
||||
"Order ID:": "訂單編號:"
|
||||
"Order ID:": "訂單編號:",
|
||||
"TTS Highlighting": "TTS 高亮",
|
||||
"Style": "樣式",
|
||||
"Underline": "底線",
|
||||
"Strikethrough": "刪除線",
|
||||
"Squiggly": "波浪線",
|
||||
"Outline": "輪廓",
|
||||
"Save Current Color": "儲存目前顏色",
|
||||
"Quick Colors": "快速顏色",
|
||||
"Highlighter": "螢光筆",
|
||||
"Save Book Cover": "保存書籍封面",
|
||||
"Auto-save last book cover": "自動保存最後一本書的封面"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.91": {
|
||||
"date": "2025-10-29",
|
||||
"notes": [
|
||||
"TTS: Fixed an issue where system voices were not working on Android",
|
||||
"TTS: Added options to customize the highlight style during read-aloud",
|
||||
"E-Ink: Disabled all shadows to improve display clarity",
|
||||
"E-Ink: Added an option to save the last book cover on Android",
|
||||
"Android: Fixed a crash that occurred when toggling 'Allow Script'",
|
||||
"Android: Adjusted launcher icon size for better compatibility across devices"
|
||||
]
|
||||
},
|
||||
"0.9.90": {
|
||||
"date": "2025-10-28",
|
||||
"notes": [
|
||||
|
||||
@@ -57,9 +57,15 @@ android {
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig = signingConfigs.getByName("signing")
|
||||
}
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
@@ -75,13 +81,13 @@ rust {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.activity:activity-ktx:1.9.3")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@mipmap/ic_launcher_foreground"
|
||||
android:inset="22%"/>
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -19,6 +19,8 @@ const COMMANDS: &[&str] = &[
|
||||
"set_screen_brightness",
|
||||
"get_external_sdcard_path",
|
||||
"request_manage_storage_permission",
|
||||
"check_permissions",
|
||||
"request_permissions",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
];
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check-permissions"
|
||||
description = "Enables the check-permissions command without any pre-configured scope."
|
||||
commands.allow = ["check-permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check-permissions"
|
||||
description = "Denies the check-permissions command without any pre-configured scope."
|
||||
commands.deny = ["check-permissions"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check-permissions"
|
||||
description = "Enables the check_permissions command without any pre-configured scope."
|
||||
commands.allow = ["check_permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check-permissions"
|
||||
description = "Denies the check_permissions command without any pre-configured scope."
|
||||
commands.deny = ["check_permissions"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-permissions"
|
||||
description = "Enables the request-permissions command without any pre-configured scope."
|
||||
commands.allow = ["request-permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-permissions"
|
||||
description = "Denies the request-permissions command without any pre-configured scope."
|
||||
commands.deny = ["request-permissions"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-permissions"
|
||||
description = "Enables the request_permissions command without any pre-configured scope."
|
||||
commands.allow = ["request_permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-permissions"
|
||||
description = "Denies the request_permissions command without any pre-configured scope."
|
||||
commands.deny = ["request_permissions"]
|
||||
+106
@@ -24,6 +24,8 @@ Default permissions for the plugin
|
||||
- `allow-set-screen-brightness`
|
||||
- `allow-get-external-sdcard-path`
|
||||
- `allow-request-manage-storage-permission`
|
||||
- `allow-check-permissions`
|
||||
- `allow-request-permissions`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
|
||||
@@ -91,6 +93,32 @@ Denies the auth_with_safari command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the check-permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the check-permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-checkPermissions`
|
||||
|
||||
</td>
|
||||
@@ -117,6 +145,32 @@ Denies the checkPermissions command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the check_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the check_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-copy-uri-to-path`
|
||||
|
||||
</td>
|
||||
@@ -481,6 +535,32 @@ Denies the lock_screen_orientation command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request-permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request-permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-requestPermissions`
|
||||
|
||||
</td>
|
||||
@@ -533,6 +613,32 @@ Denies the request_manage_storage_permission command without any pre-configured
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-set-screen-brightness`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -21,6 +21,8 @@ permissions = [
|
||||
"allow-set-screen-brightness",
|
||||
"allow-get-external-sdcard-path",
|
||||
"allow-request-manage-storage-permission",
|
||||
"allow-check-permissions",
|
||||
"allow-request-permissions",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
]
|
||||
|
||||
+50
-2
@@ -318,6 +318,18 @@
|
||||
"const": "deny-auth-with-safari",
|
||||
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check-permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check-permissions",
|
||||
"markdownDescription": "Enables the check-permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check-permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check-permissions",
|
||||
"markdownDescription": "Denies the check-permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -330,6 +342,18 @@
|
||||
"const": "deny-checkPermissions",
|
||||
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -498,6 +522,18 @@
|
||||
"const": "deny-lock-screen-orientation",
|
||||
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request-permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-permissions",
|
||||
"markdownDescription": "Enables the request-permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request-permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-permissions",
|
||||
"markdownDescription": "Denies the request-permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -522,6 +558,18 @@
|
||||
"const": "deny-request-manage-storage-permission",
|
||||
"markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-permissions",
|
||||
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-permissions",
|
||||
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_screen_brightness command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -559,10 +607,10 @@
|
||||
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ const COMMANDS: &[&str] = &[
|
||||
"set_media_session_active",
|
||||
"update_media_session_state",
|
||||
"update_media_session_metadata",
|
||||
"register_listener",
|
||||
"remove_listener",
|
||||
"check_permissions",
|
||||
"request_permissions",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
"registerListener",
|
||||
"remove_listener",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check-permissions"
|
||||
description = "Enables the check_permissions command without any pre-configured scope."
|
||||
commands.allow = ["check_permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check-permissions"
|
||||
description = "Denies the check_permissions command without any pre-configured scope."
|
||||
commands.deny = ["check_permissions"]
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-registerListener"
|
||||
description = "Enables the registerListener command without any pre-configured scope."
|
||||
commands.allow = ["registerListener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-registerListener"
|
||||
description = "Denies the registerListener command without any pre-configured scope."
|
||||
commands.deny = ["registerListener"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-register-listener"
|
||||
description = "Enables the register_listener command without any pre-configured scope."
|
||||
commands.allow = ["register_listener"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-register-listener"
|
||||
description = "Denies the register_listener command without any pre-configured scope."
|
||||
commands.deny = ["register_listener"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-permissions"
|
||||
description = "Enables the request_permissions command without any pre-configured scope."
|
||||
commands.allow = ["request_permissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-permissions"
|
||||
description = "Denies the request_permissions command without any pre-configured scope."
|
||||
commands.deny = ["request_permissions"]
|
||||
+60
-6
@@ -16,10 +16,12 @@ Default permissions for the plugin
|
||||
- `allow-set-media-session-active`
|
||||
- `allow-update-media-session-state`
|
||||
- `allow-update-media-session-metadata`
|
||||
- `allow-register-listener`
|
||||
- `allow-remove-listener`
|
||||
- `allow-check-permissions`
|
||||
- `allow-request-permissions`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
- `allow-registerListener`
|
||||
- `allow-remove-listener`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -59,6 +61,32 @@ Denies the checkPermissions command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the check_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-check-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the check_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-all-voices`
|
||||
|
||||
</td>
|
||||
@@ -137,12 +165,12 @@ Denies the pause command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-registerListener`
|
||||
`native-tts:allow-register-listener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the registerListener command without any pre-configured scope.
|
||||
Enables the register_listener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -150,12 +178,12 @@ Enables the registerListener command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-registerListener`
|
||||
`native-tts:deny-register-listener`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the registerListener command without any pre-configured scope.
|
||||
Denies the register_listener command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -215,6 +243,32 @@ Denies the requestPermissions command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-request-permissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request_permissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-resume`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -13,8 +13,10 @@ permissions = [
|
||||
"allow-set-media-session-active",
|
||||
"allow-update-media-session-state",
|
||||
"allow-update-media-session-metadata",
|
||||
"allow-register-listener",
|
||||
"allow-remove-listener",
|
||||
"allow-check-permissions",
|
||||
"allow-request-permissions",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
"allow-registerListener",
|
||||
"allow-remove-listener",
|
||||
]
|
||||
|
||||
+32
-8
@@ -306,6 +306,18 @@
|
||||
"const": "deny-checkPermissions",
|
||||
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -343,16 +355,16 @@
|
||||
"markdownDescription": "Denies the pause command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the registerListener command without any pre-configured scope.",
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-registerListener",
|
||||
"markdownDescription": "Enables the registerListener command without any pre-configured scope."
|
||||
"const": "allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the registerListener command without any pre-configured scope.",
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-registerListener",
|
||||
"markdownDescription": "Denies the registerListener command without any pre-configured scope."
|
||||
"const": "deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
@@ -378,6 +390,18 @@
|
||||
"const": "deny-requestPermissions",
|
||||
"markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-permissions",
|
||||
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-permissions",
|
||||
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -487,10 +511,10 @@
|
||||
"markdownDescription": "Denies the update_media_session_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { handleAuthCallback } from '@/helpers/auth';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { getUserProfilePlan } from '@/utils/access';
|
||||
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
|
||||
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
@@ -173,7 +173,7 @@ export default function AuthPage() {
|
||||
const type = params.get('type');
|
||||
if (accessToken) {
|
||||
let next = params.get('next') ?? '/';
|
||||
if (getUserPlan(accessToken) === 'free') {
|
||||
if (getUserProfilePlan(accessToken) === 'free') {
|
||||
next = '/user';
|
||||
}
|
||||
handleAuthCallback({ accessToken, refreshToken, type, next, login, navigate: router.push });
|
||||
|
||||
@@ -39,7 +39,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { user } = useAuth();
|
||||
const { userPlan, quotas } = useQuotaStats(true);
|
||||
const { userProfilePlan, quotas } = useQuotaStats(true);
|
||||
const { themeMode, setThemeMode } = useThemeStore();
|
||||
const { settings, setSettingsDialogOpen } = useSettingsStore();
|
||||
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
|
||||
@@ -53,6 +53,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
);
|
||||
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
|
||||
const [alwaysInForeground, setAlwaysInForeground] = useState(settings.alwaysInForeground);
|
||||
const [savedBookCoverForLockScreen, setSavedBookCoverForLockScreen] = useState(
|
||||
settings.savedBookCoverForLockScreen || '',
|
||||
);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
@@ -169,6 +172,12 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setSettingsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSetSavedBookCoverForLockScreen = () => {
|
||||
const newValue = settings.savedBookCoverForLockScreen ? '' : 'default';
|
||||
saveSysSettings(envConfig, 'savedBookCoverForLockScreen', newValue);
|
||||
setSavedBookCoverForLockScreen(newValue);
|
||||
};
|
||||
|
||||
const toggleAlwaysInForeground = async () => {
|
||||
const requestAlwaysInForeground = !settings.alwaysInForeground;
|
||||
|
||||
@@ -299,13 +308,26 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
<MenuItem label={_('Advanced Settings')}>
|
||||
<ul className='flex flex-col'>
|
||||
<MenuItem label={_('Change Data Location')} noIcon onClick={handleSetRootDir} />
|
||||
<MenuItem
|
||||
label={_('Change Data Location')}
|
||||
noIcon={!appService?.isAndroidApp}
|
||||
onClick={handleSetRootDir}
|
||||
/>
|
||||
{appService?.isAndroidApp && (
|
||||
<MenuItem
|
||||
label={_('Save Book Cover')}
|
||||
tooltip={_('Auto-save last book cover')}
|
||||
description={savedBookCoverForLockScreen ? '💾 Images/last-book-cover.png' : ''}
|
||||
toggled={!!savedBookCoverForLockScreen}
|
||||
onClick={handleSetSavedBookCoverForLockScreen}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<hr aria-hidden='true' className='border-base-200 my-1' />
|
||||
{user && userPlan === 'free' && !appService?.isIOSApp && (
|
||||
{user && userProfilePlan === 'free' && (
|
||||
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
|
||||
)}
|
||||
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
|
||||
@@ -85,12 +85,14 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const config = getConfig(bookKey);
|
||||
const progress = getProgress(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const viewState = getViewState(bookKey);
|
||||
const gridInsets = calcGridInsets(index, bookKeys.length);
|
||||
const { book, bookDoc } = bookData || {};
|
||||
if (!book || !config || !bookDoc || !viewSettings) return null;
|
||||
if (!book || !config || !bookDoc || !viewSettings || !viewState) return null;
|
||||
|
||||
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
|
||||
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
|
||||
const isBookmarked = viewState.ribbonVisible;
|
||||
const viewerKey = viewState.viewerKey;
|
||||
const horizontalGapPercent = viewSettings.gapPercent;
|
||||
const viewInsets = getViewInsets(viewSettings);
|
||||
const contentInsets = {
|
||||
@@ -123,6 +125,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
<FoliateViewer
|
||||
key={viewerKey}
|
||||
bookKey={bookKey}
|
||||
bookDoc={bookDoc}
|
||||
config={config}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { TransformContext } from '@/services/transformers/types';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { useTextTranslation } from '../hooks/useTextTranslation';
|
||||
import { useBookCoverAutoSave } from '../hooks/useAutoSaveBookCover';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import { removeTabIndex } from '@/utils/a11y';
|
||||
@@ -101,6 +102,7 @@ const FoliateViewer: React.FC<{
|
||||
useUICSS(bookKey);
|
||||
useProgressSync(bookKey);
|
||||
useProgressAutoSave(bookKey);
|
||||
useBookCoverAutoSave(bookKey);
|
||||
const { syncState, conflictDetails, resolveWithLocal, resolveWithRemote } = useKOSync(bookKey);
|
||||
useTextTranslation(bookKey, viewRef.current);
|
||||
|
||||
@@ -271,7 +273,6 @@ const FoliateViewer: React.FC<{
|
||||
await import('foliate-js/view.js');
|
||||
const view = wrappedFoliateView(document.createElement('foliate-view') as FoliateView);
|
||||
view.id = `foliate-view-${bookKey}`;
|
||||
document.body.append(view);
|
||||
containerRef.current?.appendChild(view);
|
||||
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
@@ -323,7 +323,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
setShowIndicator(true);
|
||||
const ttsController = new TTSController(appService, view);
|
||||
await ttsController.init();
|
||||
await ttsController.initViewTTS();
|
||||
await ttsController.initViewTTS(viewSettings.ttsHighlightOptions);
|
||||
const ssml = view.tts?.from(ttsFromRange);
|
||||
if (ssml) {
|
||||
const lang = parseSSMLLang(ssml, primaryLang) || 'en';
|
||||
@@ -625,6 +625,13 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
};
|
||||
}, [hoveredBookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const ttsHighlightOptions = viewSettings?.ttsHighlightOptions;
|
||||
if (ttsControllerRef.current && ttsHighlightOptions) {
|
||||
ttsControllerRef.current.initViewTTS(ttsHighlightOptions);
|
||||
}
|
||||
}, [viewSettings?.ttsHighlightOptions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showPanel && <Overlay onDismiss={handleDismissPopup} />}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
|
||||
export const useBookCoverAutoSave = (bookKey: string) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const saveBookCover = useCallback(
|
||||
throttle(
|
||||
() => {
|
||||
setTimeout(async () => {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
const bookData = useBookDataStore.getState().getBookData(bookKey);
|
||||
const book = bookData?.book;
|
||||
const savedBookHash = settings.savedBookCoverForLockScreen;
|
||||
if (appService && book && savedBookHash && savedBookHash !== book?.hash) {
|
||||
const coverPath = await appService.resolveFilePath(getCoverFilename(book), 'Books');
|
||||
try {
|
||||
await appService.copyFile(coverPath, 'last-book-cover.png', 'Images');
|
||||
settings.savedBookCoverForLockScreen = book.hash;
|
||||
useSettingsStore.getState().setSettings(settings);
|
||||
useSettingsStore.getState().saveSettings(envConfig, settings);
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-save book cover for lock screen:', error);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
5000,
|
||||
{ emitLast: false },
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
saveBookCover();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
};
|
||||
@@ -67,7 +67,7 @@ const ProfilePage = () => {
|
||||
|
||||
useTheme({ systemUIVisible: false });
|
||||
|
||||
const { quotas, userPlan = 'free' } = useQuotaStats();
|
||||
const { quotas, userProfilePlan = 'free' } = useQuotaStats();
|
||||
const { handleLogout, handleResetPassword, handleConfirmDelete } = useUserActions();
|
||||
|
||||
const { availablePlans } = useAvailablePlans({
|
||||
@@ -218,8 +218,8 @@ const ProfilePage = () => {
|
||||
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
|
||||
const userFullName = user?.user_metadata?.['full_name'] || '-';
|
||||
const userEmail = user?.email || '';
|
||||
const planDetails =
|
||||
getPlanDetails(userPlan, availablePlans) || getPlanDetails('free', availablePlans);
|
||||
const userPlanDetails =
|
||||
getPlanDetails(userProfilePlan, availablePlans) || getPlanDetails('free', availablePlans);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -258,7 +258,7 @@ const ProfilePage = () => {
|
||||
avatarUrl={avatarUrl}
|
||||
userFullName={userFullName}
|
||||
userEmail={userEmail}
|
||||
planDetails={planDetails}
|
||||
planDetails={userPlanDetails}
|
||||
/>
|
||||
|
||||
<UsageStats quotas={quotas} />
|
||||
@@ -267,14 +267,14 @@ const ProfilePage = () => {
|
||||
<div className='flex flex-col gap-y-8 sm:px-6'>
|
||||
<PlansComparison
|
||||
availablePlans={availablePlans}
|
||||
userPlan={userPlan}
|
||||
userPlan={userProfilePlan}
|
||||
onSubscribe={appService.hasIAP ? handleIAPSubscribe : handleStripeSubscribe}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-y-8 px-6'>
|
||||
<AccountActions
|
||||
userPlan={userPlan}
|
||||
userPlan={userProfilePlan}
|
||||
onLogout={handleLogout}
|
||||
onResetPassword={handleResetPassword}
|
||||
onConfirmDelete={handleDeleteWithMessage}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MdOutlineLightMode, MdOutlineDarkMode, MdClose, MdAdd } from 'react-icons/md';
|
||||
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
|
||||
import { CgColorPicker } from 'react-icons/cg';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import {
|
||||
applyCustomTheme,
|
||||
CustomTheme,
|
||||
@@ -17,48 +12,55 @@ import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useResetViewSettings } from '@/hooks/useResetSettings';
|
||||
import { useCustomTextureStore } from '@/store/customTextureStore';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import { useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { PREDEFINED_TEXTURES } from '@/styles/textures';
|
||||
import { HighlightColor } from '@/types/book';
|
||||
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
|
||||
import Select from '@/components/Select';
|
||||
import ThemeEditor from './ThemeEditor';
|
||||
import ColorInput from './ColorInput';
|
||||
import ThemeEditor from './color/ThemeEditor';
|
||||
import ThemeModeSelector from './color/ThemeModeSelector';
|
||||
import ThemeColorSelector from './color/ThemeColorSelector';
|
||||
import BackgroundTextureSelector from './color/BackgroundTextureSelector';
|
||||
import HighlightColorsEditor from './color/HighlightColorsEditor';
|
||||
import TTSHighlightStyleEditor, { TTSHighlightStyle } from './color/TTSHighlightStyleEditor';
|
||||
import CodeHighlightingSettings from './color/CodeHighlightingSettings';
|
||||
|
||||
const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
|
||||
const _ = useTranslation();
|
||||
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
|
||||
useThemeStore();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
|
||||
|
||||
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
|
||||
viewSettings.invertImgColorInDark,
|
||||
);
|
||||
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const iconSize24 = useResponsiveSize(24);
|
||||
const [editTheme, setEditTheme] = useState<CustomTheme | null>(null);
|
||||
const [customThemes, setCustomThemes] = useState<Theme[]>([]);
|
||||
const [showCustomThemeEditor, setShowCustomThemeEditor] = useState(false);
|
||||
const [overrideColor, setOverrideColor] = useState(viewSettings.overrideColor);
|
||||
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting);
|
||||
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage);
|
||||
|
||||
const [selectedTextureId, setSelectedTextureId] = useState(viewSettings.backgroundTextureId);
|
||||
const [backgroundOpacity, setBackgroundOpacity] = useState(viewSettings.backgroundOpacity);
|
||||
const [backgroundSize, setBackgroundSize] = useState(viewSettings.backgroundSize);
|
||||
|
||||
const [ttsHighlightStyle, setTtsHighlightStyle] = useState(
|
||||
viewSettings.ttsHighlightOptions.style,
|
||||
);
|
||||
const [ttsHighlightColor, setTtsHighlightColor] = useState(
|
||||
viewSettings.ttsHighlightOptions.color,
|
||||
);
|
||||
const [customHighlightColors, setCustomHighlightColors] = useState(
|
||||
settings.globalReadSettings.customHighlightColors,
|
||||
);
|
||||
const [customTtsHighlightColors, setCustomTtsHighlightColors] = useState(
|
||||
settings.globalReadSettings.customTtsHighlightColors || [],
|
||||
);
|
||||
|
||||
const {
|
||||
textures: customTextures,
|
||||
@@ -70,7 +72,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
saveCustomTextures,
|
||||
} = useCustomTextureStore();
|
||||
const resetToDefaults = useResetViewSettings();
|
||||
|
||||
const { selectFiles } = useFileSelector(appService, _);
|
||||
|
||||
const handleReset = () => {
|
||||
@@ -110,7 +111,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
}, [overrideColor]);
|
||||
|
||||
useEffect(() => {
|
||||
let update = false; // check if we need to update syntax highlighting
|
||||
let update = false;
|
||||
if (codeHighlighting !== viewSettings.codeHighlighting) {
|
||||
saveViewSettings(envConfig, bookKey, 'codeHighlighting', codeHighlighting);
|
||||
update = true;
|
||||
@@ -172,7 +173,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
const handleSaveCustomTheme = (customTheme: CustomTheme) => {
|
||||
applyCustomTheme(customTheme);
|
||||
saveCustomTheme(envConfig, settings, customTheme);
|
||||
|
||||
setSettings({ ...settings });
|
||||
setThemeColor(customTheme.name);
|
||||
setShowCustomThemeEditor(false);
|
||||
@@ -180,7 +180,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
|
||||
const handleDeleteCustomTheme = (customTheme: CustomTheme) => {
|
||||
saveCustomTheme(envConfig, settings, customTheme, true);
|
||||
|
||||
setSettings({ ...settings });
|
||||
setThemeColor('default');
|
||||
setShowCustomThemeEditor(false);
|
||||
@@ -224,7 +223,35 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
saveCustomTextures(envConfig);
|
||||
};
|
||||
|
||||
const allTextures = [...PREDEFINED_TEXTURES, ...customTextures.filter((t) => !t.deletedAt)];
|
||||
const handleHighlightColorsChange = (colors: typeof customHighlightColors) => {
|
||||
setCustomHighlightColors(colors);
|
||||
settings.globalReadSettings.customHighlightColors = colors;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
const handleTTSStyleChange = (style: TTSHighlightStyle) => {
|
||||
setTtsHighlightStyle(style);
|
||||
saveViewSettings(envConfig, bookKey, 'ttsHighlightOptions', {
|
||||
style,
|
||||
color: ttsHighlightColor,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTTSColorChange = (color: string) => {
|
||||
setTtsHighlightColor(color);
|
||||
saveViewSettings(envConfig, bookKey, 'ttsHighlightOptions', {
|
||||
style: ttsHighlightStyle,
|
||||
color,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCustomTtsColorsChange = (colors: string[]) => {
|
||||
setCustomTtsHighlightColors(colors);
|
||||
settings.globalReadSettings.customTtsHighlightColors = colors;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
@@ -237,32 +264,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Theme Mode')}</h2>
|
||||
<div className='flex gap-4'>
|
||||
<button
|
||||
title={_('Auto Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setThemeMode('auto')}
|
||||
>
|
||||
<TbSunMoon />
|
||||
</button>
|
||||
<button
|
||||
title={_('Light Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setThemeMode('light')}
|
||||
>
|
||||
<MdOutlineLightMode />
|
||||
</button>
|
||||
<button
|
||||
title={_('Dark Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setThemeMode('dark')}
|
||||
>
|
||||
<MdOutlineDarkMode />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeModeSelector themeMode={themeMode} onThemeModeChange={setThemeMode} />
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Invert Image In Dark Mode')}</h2>
|
||||
@@ -285,226 +287,48 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
{themes.concat(customThemes).map(({ name, label, colors, isCustomizale }) => (
|
||||
<button
|
||||
key={name}
|
||||
tabIndex={0}
|
||||
onClick={() => setThemeColor(name)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
setThemeColor(name);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
|
||||
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isDarkMode
|
||||
? colors.dark['base-100']
|
||||
: colors.light['base-100'],
|
||||
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label={_(label)}
|
||||
type='radio'
|
||||
name='theme'
|
||||
value={name}
|
||||
checked={themeColor === name}
|
||||
onChange={() => setThemeColor(name)}
|
||||
className='hidden'
|
||||
/>
|
||||
{themeColor === name ? (
|
||||
<MdRadioButtonChecked size={iconSize24} />
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={iconSize24} />
|
||||
)}
|
||||
<span>{_(label)}</span>
|
||||
{isCustomizale && themeColor === name && (
|
||||
<button onClick={() => handleEditTheme(name)}>
|
||||
<CgColorPicker size={iconSize16} className='absolute right-2 top-2' />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-4 shadow-md`}
|
||||
onClick={() => setShowCustomThemeEditor(true)}
|
||||
>
|
||||
<PiPlus size={iconSize24} />
|
||||
<span>{_('Custom')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeColorSelector
|
||||
themes={themes.concat(customThemes)}
|
||||
themeColor={themeColor}
|
||||
isDarkMode={isDarkMode}
|
||||
onThemeColorChange={setThemeColor}
|
||||
onEditTheme={handleEditTheme}
|
||||
onCreateTheme={() => setShowCustomThemeEditor(true)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Background Image')}</h2>
|
||||
<div className='mb-4 grid grid-cols-2 gap-4'>
|
||||
{allTextures.map((texture) => (
|
||||
<button
|
||||
key={texture.id}
|
||||
onClick={() => setSelectedTextureId(texture.id)}
|
||||
className={`bg-base-100 relative flex flex-col items-center justify-center rounded-lg border-2 p-4 shadow-md transition-all ${
|
||||
selectedTextureId === texture.id
|
||||
? 'ring-2 ring-indigo-500 ring-offset-2'
|
||||
: 'border-base-300'
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: texture.loaded
|
||||
? `url("${texture.blobUrl || texture.url}")`
|
||||
: 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'top',
|
||||
minHeight: '80px',
|
||||
}}
|
||||
>
|
||||
{selectedTextureId === texture.id && (
|
||||
<MdRadioButtonChecked
|
||||
size={iconSize24}
|
||||
className='absolute right-2 top-2 rounded-full bg-white text-indigo-500'
|
||||
/>
|
||||
)}
|
||||
{!PREDEFINED_TEXTURES.find((t) => t.id === texture.id) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCustomTexture(texture.id);
|
||||
}}
|
||||
className='absolute left-2 top-2 rounded-full bg-red-500 p-1 text-white transition-colors hover:bg-red-600'
|
||||
title={_('Delete')}
|
||||
>
|
||||
<MdClose size={16} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<BackgroundTextureSelector
|
||||
predefinedTextures={PREDEFINED_TEXTURES}
|
||||
customTextures={customTextures.filter((t) => !t.deletedAt)}
|
||||
selectedTextureId={selectedTextureId}
|
||||
backgroundOpacity={backgroundOpacity}
|
||||
backgroundSize={backgroundSize}
|
||||
onTextureSelect={setSelectedTextureId}
|
||||
onOpacityChange={setBackgroundOpacity}
|
||||
onSizeChange={setBackgroundSize}
|
||||
onImportImage={handleImportImage}
|
||||
onDeleteTexture={handleDeleteCustomTexture}
|
||||
/>
|
||||
|
||||
{/* Custom Image Upload */}
|
||||
<div
|
||||
className={`border-base-300 relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 shadow-md transition-all`}
|
||||
style={{ minHeight: '80px' }}
|
||||
>
|
||||
<button
|
||||
className='card-body flex cursor-pointer items-center justify-center p-2 text-center'
|
||||
onClick={handleImportImage}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<MdAdd className='text-primary/85 group-hover:text-primary h-6 w-6' />
|
||||
</div>
|
||||
<div className='text-primary/85 group-hover:text-primary line-clamp-1 font-medium'>
|
||||
{_('Import Image')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<HighlightColorsEditor
|
||||
customHighlightColors={customHighlightColors}
|
||||
onChange={handleHighlightColorsChange}
|
||||
/>
|
||||
|
||||
{/* Background Image Settings */}
|
||||
{selectedTextureId !== 'none' && (
|
||||
<div className='card border-base-200 bg-base-100 space-y-4 border p-4 shadow'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Opacity')}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<input
|
||||
type='range'
|
||||
min='0'
|
||||
max='1'
|
||||
step='0.05'
|
||||
value={backgroundOpacity}
|
||||
onChange={(e) => setBackgroundOpacity(parseFloat(e.target.value))}
|
||||
className='range range-sm w-32'
|
||||
/>
|
||||
<span className='w-12 text-right text-sm'>
|
||||
{Math.round(backgroundOpacity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<TTSHighlightStyleEditor
|
||||
style={ttsHighlightStyle}
|
||||
color={ttsHighlightColor}
|
||||
customColors={customTtsHighlightColors}
|
||||
onStyleChange={handleTTSStyleChange}
|
||||
onColorChange={handleTTSColorChange}
|
||||
onCustomColorsChange={handleCustomTtsColorsChange}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Size')}</span>
|
||||
<Select
|
||||
value={backgroundSize}
|
||||
onChange={(e) => setBackgroundSize(e.target.value)}
|
||||
options={[
|
||||
{ value: 'auto', label: _('Auto') },
|
||||
{ value: 'cover', label: _('Cover') },
|
||||
{ value: 'contain', label: _('Contain') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Highlight Colors')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 overflow-visible border p-4 shadow'>
|
||||
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
|
||||
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
|
||||
(color, index, array) => {
|
||||
const position =
|
||||
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
|
||||
return (
|
||||
<div key={color} className='flex flex-col items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: customHighlightColors[color] }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={customHighlightColors[color]}
|
||||
compact={true}
|
||||
pickerPosition={position}
|
||||
onChange={(value: string) => {
|
||||
customHighlightColors[color] = value;
|
||||
setCustomHighlightColors({ ...customHighlightColors });
|
||||
settings.globalReadSettings.customHighlightColors =
|
||||
customHighlightColors;
|
||||
setSettings(settings);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Code Highlighting')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Enable Highlighting')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={codeHighlighting}
|
||||
onChange={() => setcodeHighlighting(!codeHighlighting)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Code Language')}</span>
|
||||
<Select
|
||||
value={codeLanguage}
|
||||
onChange={(event) => setCodeLanguage(event.target.value as CodeLanguage)}
|
||||
options={CODE_LANGUAGES.map((lang) => ({
|
||||
value: lang,
|
||||
label: lang === 'auto-detect' ? _('Auto') : lang,
|
||||
}))}
|
||||
disabled={!codeHighlighting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CodeHighlightingSettings
|
||||
codeHighlighting={codeHighlighting}
|
||||
codeLanguage={codeLanguage}
|
||||
onToggle={setcodeHighlighting}
|
||||
onLanguageChange={setCodeLanguage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,16 +8,16 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResetViewSettings } from '@/hooks/useResetSettings';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { saveSysSettings, saveViewSettings } from '@/helpers/settings';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import { RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import NumberInput from './NumberInput';
|
||||
|
||||
const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const { getView, getViewSettings, recreateViewer } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { applyEinkMode } = useEinkMode();
|
||||
@@ -144,7 +144,9 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
useEffect(() => {
|
||||
if (viewSettings.allowScript === allowScript) return;
|
||||
saveViewSettings(envConfig, bookKey, 'allowScript', allowScript, true, false);
|
||||
saveAndReload();
|
||||
setTimeout(() => {
|
||||
recreateViewer(envConfig, bookKey);
|
||||
}, RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allowScript]);
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { getTranslators } from '@/services/translators';
|
||||
import { useResetViewSettings } from '@/hooks/useResetSettings';
|
||||
import { TRANSLATED_LANGS, TRANSLATOR_LANGS } from '@/services/constants';
|
||||
import {
|
||||
RELOAD_BEFORE_SAVED_TIMEOUT_MS,
|
||||
TRANSLATED_LANGS,
|
||||
TRANSLATOR_LANGS,
|
||||
} from '@/services/constants';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
|
||||
@@ -18,7 +21,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
const { token } = useAuth();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, applyUILanguage } = useSettingsStore();
|
||||
const { getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getViewSettings, setViewSettings, recreateViewer } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
|
||||
|
||||
const [uiLanguage, setUILanguage] = useState(viewSettings.uiLanguage);
|
||||
@@ -141,10 +144,8 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
useEffect(() => {
|
||||
if (translationEnabled === viewSettings.translationEnabled) return;
|
||||
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
|
||||
viewSettings.translationEnabled = translationEnabled;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
if (!showTranslateSource && !translationEnabled) {
|
||||
saveAndReload();
|
||||
if (!showTranslateSource && translationEnabled) {
|
||||
setTimeout(() => recreateViewer(envConfig, bookKey), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [translationEnabled]);
|
||||
@@ -152,7 +153,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
useEffect(() => {
|
||||
if (showTranslateSource === viewSettings.showTranslateSource) return;
|
||||
saveViewSettings(envConfig, bookKey, 'showTranslateSource', showTranslateSource, false, false);
|
||||
saveAndReload();
|
||||
setTimeout(() => recreateViewer(envConfig, bookKey), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showTranslateSource]);
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResetViewSettings } from '@/hooks/useResetSettings';
|
||||
import { isCJKEnv } from '@/utils/misc';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { saveAndReload } from '@/utils/reload';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
|
||||
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
|
||||
import { MIGHT_BE_RTL_LANGS, RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
import { SettingsPanelPanelProp } from './SettingsDialog';
|
||||
import Select from '@/components/Select';
|
||||
import NumberInput from './NumberInput';
|
||||
@@ -26,7 +25,8 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
|
||||
const { getView, getViewSettings, getGridInsets } = useReaderStore();
|
||||
const { setViewSettings, recreateViewer } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
|
||||
|
||||
@@ -283,7 +283,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
(['horizontal-rl', 'vertical-rl'].includes(writingMode) ||
|
||||
['horizontal-rl', 'vertical-rl'].includes(prevWritingMode))
|
||||
) {
|
||||
saveAndReload();
|
||||
setTimeout(() => recreateViewer(envConfig, bookKey), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [writingMode]);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import { MdRadioButtonChecked, MdClose, MdAdd } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
interface Texture {
|
||||
id: string;
|
||||
url?: string;
|
||||
blobUrl?: string;
|
||||
loaded?: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundTextureSelectorProps {
|
||||
predefinedTextures: Texture[];
|
||||
customTextures: Texture[];
|
||||
selectedTextureId: string;
|
||||
backgroundOpacity: number;
|
||||
backgroundSize: string;
|
||||
onTextureSelect: (id: string) => void;
|
||||
onOpacityChange: (opacity: number) => void;
|
||||
onSizeChange: (size: string) => void;
|
||||
onImportImage: () => void;
|
||||
onDeleteTexture: (id: string) => void;
|
||||
}
|
||||
|
||||
const BackgroundTextureSelector: React.FC<BackgroundTextureSelectorProps> = ({
|
||||
predefinedTextures,
|
||||
customTextures,
|
||||
selectedTextureId,
|
||||
backgroundOpacity,
|
||||
backgroundSize,
|
||||
onTextureSelect,
|
||||
onOpacityChange,
|
||||
onSizeChange,
|
||||
onImportImage,
|
||||
onDeleteTexture,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize24 = useResponsiveSize(24);
|
||||
|
||||
const allTextures = [...predefinedTextures, ...customTextures];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Background Image')}</h2>
|
||||
<div className='mb-4 grid grid-cols-2 gap-4'>
|
||||
{allTextures.map((texture) => (
|
||||
<button
|
||||
key={texture.id}
|
||||
onClick={() => onTextureSelect(texture.id)}
|
||||
className={`bg-base-100 relative flex flex-col items-center justify-center rounded-lg border-2 p-4 shadow-md transition-all ${
|
||||
selectedTextureId === texture.id
|
||||
? 'ring-2 ring-indigo-500 ring-offset-2'
|
||||
: 'border-base-300'
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: texture.loaded ? `url("${texture.blobUrl || texture.url}")` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'top',
|
||||
minHeight: '80px',
|
||||
}}
|
||||
>
|
||||
{selectedTextureId === texture.id && (
|
||||
<MdRadioButtonChecked
|
||||
size={iconSize24}
|
||||
className='absolute right-2 top-2 rounded-full bg-white text-indigo-500'
|
||||
/>
|
||||
)}
|
||||
{!predefinedTextures.find((t) => t.id === texture.id) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTexture(texture.id);
|
||||
}}
|
||||
className='absolute left-2 top-2 rounded-full bg-red-500 p-1 text-white transition-colors hover:bg-red-600'
|
||||
title={_('Delete')}
|
||||
>
|
||||
<MdClose size={16} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Custom Image Upload */}
|
||||
<div
|
||||
className='border-base-300 relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 shadow-md transition-all'
|
||||
style={{ minHeight: '80px' }}
|
||||
>
|
||||
<button
|
||||
className='card-body flex cursor-pointer items-center justify-center p-2 text-center'
|
||||
onClick={onImportImage}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<MdAdd className='text-primary/85 group-hover:text-primary h-6 w-6' />
|
||||
</div>
|
||||
<div className='text-primary/85 group-hover:text-primary line-clamp-1 font-medium'>
|
||||
{_('Import Image')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Image Settings */}
|
||||
{selectedTextureId !== 'none' && (
|
||||
<div className='card border-base-200 bg-base-100 space-y-4 border p-4 shadow'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Opacity')}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<input
|
||||
type='range'
|
||||
min='0'
|
||||
max='1'
|
||||
step='0.05'
|
||||
value={backgroundOpacity}
|
||||
onChange={(e) => onOpacityChange(parseFloat(e.target.value))}
|
||||
className='range range-sm w-32'
|
||||
/>
|
||||
<span className='w-12 text-right text-sm'>
|
||||
{Math.round(backgroundOpacity * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Size')}</span>
|
||||
<Select
|
||||
value={backgroundSize}
|
||||
onChange={(e) => onSizeChange(e.target.value)}
|
||||
options={[
|
||||
{ value: 'auto', label: _('Auto') },
|
||||
{ value: 'cover', label: _('Cover') },
|
||||
{ value: 'contain', label: _('Contain') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackgroundTextureSelector;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { CODE_LANGUAGES, CodeLanguage } from '@/utils/highlightjs';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Select from '@/components/Select';
|
||||
|
||||
interface CodeHighlightingSettingsProps {
|
||||
codeHighlighting: boolean;
|
||||
codeLanguage: string;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onLanguageChange: (language: CodeLanguage) => void;
|
||||
}
|
||||
|
||||
const CodeHighlightingSettings: React.FC<CodeHighlightingSettingsProps> = ({
|
||||
codeHighlighting,
|
||||
codeLanguage,
|
||||
onToggle,
|
||||
onLanguageChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('Code Highlighting')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
<div className='config-item'>
|
||||
<span>{_('Enable Highlighting')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={codeHighlighting}
|
||||
onChange={() => onToggle(!codeHighlighting)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<span>{_('Code Language')}</span>
|
||||
<Select
|
||||
value={codeLanguage}
|
||||
onChange={(event) => onLanguageChange(event.target.value as CodeLanguage)}
|
||||
options={CODE_LANGUAGES.map((lang) => ({
|
||||
value: lang,
|
||||
label: lang === 'auto-detect' ? _('Auto') : lang,
|
||||
}))}
|
||||
disabled={!codeHighlighting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeHighlightingSettings;
|
||||
+10
-4
@@ -9,7 +9,13 @@ type ColorInputProps = {
|
||||
pickerPosition?: 'left' | 'center' | 'right';
|
||||
};
|
||||
|
||||
const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange, compact = false, pickerPosition = 'left' }) => {
|
||||
const ColorInput: React.FC<ColorInputProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
compact = false,
|
||||
pickerPosition = 'left',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -53,9 +59,9 @@ const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange, compact
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={pickerRef}
|
||||
className={`absolute top-full z-50 mt-1 ${getPickerPositionClass()}`}
|
||||
<div
|
||||
ref={pickerRef}
|
||||
className={`absolute top-full z-50 py-1 ${getPickerPositionClass()}`}
|
||||
>
|
||||
<SketchPicker
|
||||
width='200px'
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { HighlightColor } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import ColorInput from './ColorInput';
|
||||
|
||||
interface HighlightColorsEditorProps {
|
||||
customHighlightColors: Record<HighlightColor, string>;
|
||||
onChange: (colors: Record<HighlightColor, string>) => void;
|
||||
}
|
||||
|
||||
const HighlightColorsEditor: React.FC<HighlightColorsEditorProps> = ({
|
||||
customHighlightColors,
|
||||
onChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const handleColorChange = (color: HighlightColor, value: string) => {
|
||||
const updated = { ...customHighlightColors, [color]: value };
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Highlight Colors')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 overflow-visible border p-4 shadow'>
|
||||
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
|
||||
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
|
||||
(color, index, array) => {
|
||||
const position =
|
||||
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
|
||||
return (
|
||||
<div key={color} className='flex flex-col items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: customHighlightColors[color] }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={customHighlightColors[color]}
|
||||
compact={true}
|
||||
pickerPosition={position}
|
||||
onChange={(value: string) => handleColorChange(color, value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HighlightColorsEditor;
|
||||
@@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Select from '@/components/Select';
|
||||
import ColorInput from './ColorInput';
|
||||
|
||||
export type TTSHighlightStyle =
|
||||
| 'highlight'
|
||||
| 'underline'
|
||||
| 'strikethrough'
|
||||
| 'squiggly'
|
||||
| 'outline';
|
||||
|
||||
interface TTSHighlightStyleEditorProps {
|
||||
style: TTSHighlightStyle;
|
||||
color: string;
|
||||
customColors: string[];
|
||||
onStyleChange: (style: TTSHighlightStyle) => void;
|
||||
onColorChange: (color: string) => void;
|
||||
onCustomColorsChange: (colors: string[]) => void;
|
||||
}
|
||||
|
||||
const TTSHighlightStyleEditor: React.FC<TTSHighlightStyleEditorProps> = ({
|
||||
style,
|
||||
color,
|
||||
customColors,
|
||||
onStyleChange,
|
||||
onColorChange,
|
||||
onCustomColorsChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const defaultQuickColors = [
|
||||
{ color: '#FFD700', label: 'Gold' },
|
||||
{ color: '#00CED1', label: 'Cyan' },
|
||||
{ color: '#FF69B4', label: 'Pink' },
|
||||
{ color: '#90EE90', label: 'Green' },
|
||||
{ color: '#FFA500', label: 'Orange' },
|
||||
];
|
||||
|
||||
const handleAddCustomColor = () => {
|
||||
if (!customColors.includes(color) && !defaultQuickColors.some((c) => c.color === color)) {
|
||||
const updatedColors = [...customColors, color];
|
||||
onCustomColorsChange(updatedColors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCustomColor = (colorToDelete: string) => {
|
||||
const updatedColors = customColors.filter((c) => c !== colorToDelete);
|
||||
onCustomColorsChange(updatedColors);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>{_('TTS Highlighting')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border p-4 shadow'>
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Style')}</span>
|
||||
<Select
|
||||
value={style}
|
||||
onChange={(e) => onStyleChange(e.target.value as TTSHighlightStyle)}
|
||||
options={[
|
||||
{ value: 'highlight', label: _('Highlighter') },
|
||||
{ value: 'underline', label: _('Underline') },
|
||||
{ value: 'strikethrough', label: _('Strikethrough') },
|
||||
{ value: 'squiggly', label: _('Squiggly') },
|
||||
{ value: 'outline', label: _('Outline') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>{_('Color')}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div
|
||||
className='border-base-300 pointer-events-none h-8 w-8 cursor-pointer rounded-full border-2 shadow-sm'
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<ColorInput
|
||||
label=''
|
||||
value={color}
|
||||
compact={true}
|
||||
pickerPosition='center'
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddCustomColor}
|
||||
disabled={
|
||||
customColors.includes(color) || defaultQuickColors.some((c) => c.color === color)
|
||||
}
|
||||
className='btn btn-ghost btn-sm gap-1 bg-transparent disabled:bg-transparent'
|
||||
title={_('Save Current Color')}
|
||||
>
|
||||
<span className='text-xs'>{_('Save')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-base-200 flex justify-between border-t pt-4'>
|
||||
<div className='flex items-center'>
|
||||
<span className='text-sm font-medium'>{_('Quick Colors')}</span>
|
||||
</div>
|
||||
|
||||
<div className='flex max-w-[70%] flex-wrap justify-end gap-2'>
|
||||
{defaultQuickColors.map(({ color: quickColor }) => (
|
||||
<button
|
||||
key={quickColor}
|
||||
onClick={() => onColorChange(quickColor)}
|
||||
className={`h-8 w-8 rounded-full border-2 transition-all hover:scale-110 ${
|
||||
color === quickColor
|
||||
? 'ring-2 ring-indigo-500 ring-offset-2'
|
||||
: 'border-base-300'
|
||||
}`}
|
||||
style={{ backgroundColor: quickColor }}
|
||||
/>
|
||||
))}
|
||||
|
||||
{customColors.map((customColor) => (
|
||||
<div key={customColor} className='group relative'>
|
||||
<button
|
||||
onClick={() => onColorChange(customColor)}
|
||||
className={`h-8 w-8 rounded-full border-2 transition-all hover:scale-110 ${
|
||||
color === customColor
|
||||
? 'ring-2 ring-indigo-500 ring-offset-2'
|
||||
: 'border-base-300'
|
||||
}`}
|
||||
style={{ backgroundColor: customColor }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleDeleteCustomColor(customColor)}
|
||||
className='absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white opacity-0 transition-opacity hover:opacity-100 group-hover:opacity-100'
|
||||
title={_('Delete')}
|
||||
>
|
||||
<MdClose size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSHighlightStyleEditor;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { CgColorPicker } from 'react-icons/cg';
|
||||
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Theme } from '@/styles/themes';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface ThemeColorSelectorProps {
|
||||
themes: Theme[];
|
||||
themeColor: string;
|
||||
isDarkMode: boolean;
|
||||
onThemeColorChange: (name: string) => void;
|
||||
onEditTheme: (name: string) => void;
|
||||
onCreateTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeColorSelector: React.FC<ThemeColorSelectorProps> = ({
|
||||
themes,
|
||||
themeColor,
|
||||
isDarkMode,
|
||||
onThemeColorChange,
|
||||
onEditTheme,
|
||||
onCreateTheme,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const iconSize24 = useResponsiveSize(24);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
{themes.map(({ name, label, colors, isCustomizale }) => (
|
||||
<button
|
||||
key={name}
|
||||
tabIndex={0}
|
||||
onClick={() => onThemeColorChange(name)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onThemeColorChange(name);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
|
||||
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? colors.dark['base-100'] : colors.light['base-100'],
|
||||
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
|
||||
}}
|
||||
>
|
||||
<input
|
||||
aria-label={_(label)}
|
||||
type='radio'
|
||||
name='theme'
|
||||
value={name}
|
||||
checked={themeColor === name}
|
||||
onChange={() => onThemeColorChange(name)}
|
||||
className='hidden'
|
||||
/>
|
||||
{themeColor === name ? (
|
||||
<MdRadioButtonChecked size={iconSize24} />
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={iconSize24} />
|
||||
)}
|
||||
<span>{_(label)}</span>
|
||||
{isCustomizale && themeColor === name && (
|
||||
<button onClick={() => onEditTheme(name)}>
|
||||
<CgColorPicker size={iconSize16} className='absolute right-2 top-2' />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className='relative flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-4 shadow-md'
|
||||
onClick={onCreateTheme}
|
||||
>
|
||||
<PiPlus size={iconSize24} />
|
||||
<span>{_('Custom')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeColorSelector;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { MdOutlineLightMode, MdOutlineDarkMode } from 'react-icons/md';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface ThemeModeSelectorProps {
|
||||
themeMode: 'auto' | 'light' | 'dark';
|
||||
onThemeModeChange: (mode: 'auto' | 'light' | 'dark') => void;
|
||||
}
|
||||
|
||||
const ThemeModeSelector: React.FC<ThemeModeSelectorProps> = ({ themeMode, onThemeModeChange }) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>{_('Theme Mode')}</h2>
|
||||
<div className='flex gap-4'>
|
||||
<button
|
||||
title={_('Auto Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => onThemeModeChange('auto')}
|
||||
>
|
||||
<TbSunMoon />
|
||||
</button>
|
||||
<button
|
||||
title={_('Light Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => onThemeModeChange('light')}
|
||||
>
|
||||
<MdOutlineLightMode />
|
||||
</button>
|
||||
<button
|
||||
title={_('Dark Mode')}
|
||||
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => onThemeModeChange('dark')}
|
||||
>
|
||||
<MdOutlineDarkMode />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModeSelector;
|
||||
@@ -7,6 +7,7 @@ export const useEinkMode = () => {
|
||||
} else {
|
||||
document.body.classList.remove('no-transitions');
|
||||
}
|
||||
document.documentElement.setAttribute('data-eink', isEink.toString());
|
||||
}, []);
|
||||
|
||||
return { applyEinkMode };
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { QuotaType, UserPlan } from '@/types/quota';
|
||||
import { getStoragePlanData, getTranslationPlanData, getUserPlan } from '@/utils/access';
|
||||
import { getStoragePlanData, getTranslationPlanData, getUserProfilePlan } from '@/utils/access';
|
||||
import { useTranslation } from './useTranslation';
|
||||
|
||||
export const useQuotaStats = (briefName = false) => {
|
||||
const _ = useTranslation();
|
||||
const { token, user } = useAuth();
|
||||
const [quotas, setQuotas] = useState<QuotaType[]>([]);
|
||||
const [userPlan, setUserPlan] = useState<UserPlan | undefined>(undefined);
|
||||
const [userProfilePlan, setUserProfilePlan] = useState<UserPlan | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
|
||||
const userPlan = getUserPlan(token);
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
const inGB = storagPlan.quota > 1e9;
|
||||
const storageQuota: QuotaType = {
|
||||
@@ -35,13 +34,13 @@ export const useQuotaStats = (briefName = false) => {
|
||||
total: Math.round(translationPlan.quota / 1024),
|
||||
unit: 'K',
|
||||
};
|
||||
setUserPlan(userPlan);
|
||||
setUserProfilePlan(getUserProfilePlan(token));
|
||||
setQuotas([storageQuota, translationQuota]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
return {
|
||||
quotas,
|
||||
userPlan,
|
||||
userProfilePlan,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,11 @@ import crypto from 'crypto';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { corsAllMethods, runMiddleware } from '@/utils/cors';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { getDailyTranslationPlanData, getUserPlan, validateUserAndToken } from '@/utils/access';
|
||||
import {
|
||||
getDailyTranslationPlanData,
|
||||
getSubscriptionPlan,
|
||||
validateUserAndToken,
|
||||
} from '@/utils/access';
|
||||
import { ErrorCodes } from '@/services/translators';
|
||||
import { UsageStatsManager } from '@/utils/usage';
|
||||
|
||||
@@ -53,7 +57,7 @@ const updateDailyUsage = async (
|
||||
if (!userId || !token) return 0;
|
||||
|
||||
try {
|
||||
const userPlan = getUserPlan(token);
|
||||
const userPlan = getSubscriptionPlan(token);
|
||||
const newUsage = await UsageStatsManager.trackUsage(
|
||||
userId,
|
||||
'translation_chars',
|
||||
@@ -90,7 +94,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let deeplApiUrl = deepFreeApiUrl;
|
||||
let userPlan = 'free';
|
||||
if (user && token) {
|
||||
userPlan = getUserPlan(token);
|
||||
userPlan = getSubscriptionPlan(token);
|
||||
if (userPlan === 'pro') deeplApiUrl = deeplProApiUrl;
|
||||
}
|
||||
const deeplAuthKey =
|
||||
|
||||
@@ -104,6 +104,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
squiggly: 'blue',
|
||||
},
|
||||
customHighlightColors: HIGHLIGHT_COLOR_HEX,
|
||||
customTtsHighlightColors: [],
|
||||
};
|
||||
|
||||
export const DEFAULT_MOBILE_READSETTINGS: Partial<ReadSettings> = {
|
||||
@@ -219,6 +220,7 @@ export const DEFAULT_TTS_CONFIG: TTSConfig = {
|
||||
ttsVoice: '',
|
||||
ttsLocation: '',
|
||||
showTTSBar: false,
|
||||
ttsHighlightOptions: { style: 'highlight', color: '#808080' },
|
||||
};
|
||||
|
||||
export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getDirPath, getFilename } from '@/utils/path';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
import { copyFiles } from '@/utils/files';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
import {
|
||||
@@ -78,7 +79,8 @@ const getPathResolver = ({
|
||||
const getCustomBasePrefixSync = isCustomBaseDir
|
||||
? (baseDir: BaseDir) => {
|
||||
return () => {
|
||||
const leafDir = ['Settings', 'Data', 'Books', 'Fonts'].includes(baseDir) ? '' : baseDir;
|
||||
const dataDirs = ['Settings', 'Data', 'Books', 'Fonts', 'Images'];
|
||||
const leafDir = dataDirs.includes(baseDir) ? '' : baseDir;
|
||||
return leafDir ? `${customRootDir}/${leafDir}` : customRootDir!;
|
||||
};
|
||||
}
|
||||
@@ -392,6 +394,33 @@ export class NativeAppService extends BaseAppService {
|
||||
});
|
||||
}
|
||||
await this.prepareBooksDir();
|
||||
await this.runMigrations();
|
||||
}
|
||||
|
||||
private CURRENT_MIGRATION_VERSION = 20251029;
|
||||
|
||||
private async runMigrations() {
|
||||
try {
|
||||
const settings = await this.loadSettings();
|
||||
const lastMigrationVersion = settings.migrationVersion || 0;
|
||||
|
||||
if (lastMigrationVersion < 20251029) {
|
||||
try {
|
||||
await this.migrate20251029();
|
||||
} catch (error) {
|
||||
console.error('Error migrating to version 20251029:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastMigrationVersion < this.CURRENT_MIGRATION_VERSION) {
|
||||
await this.saveSettings({
|
||||
...settings,
|
||||
migrationVersion: this.CURRENT_MIGRATION_VERSION,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to run migrations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
|
||||
@@ -422,4 +451,16 @@ export class NativeAppService extends BaseAppService {
|
||||
});
|
||||
return Array.isArray(selected) ? selected : selected ? [selected] : [];
|
||||
}
|
||||
|
||||
async migrate20251029() {
|
||||
console.log('Running migration 20251029 to update paths in Images dir...');
|
||||
const rootPath = await this.resolveFilePath('..', 'Data');
|
||||
const newDir = await this.fs.getPrefix('Images');
|
||||
const oldDir = await join(rootPath, 'Images', 'Readest', 'Images');
|
||||
|
||||
await copyFiles(this, oldDir, newDir);
|
||||
|
||||
const dirToDelete = await join(rootPath, 'Images', 'Readest');
|
||||
await this.deleteDir(dirToDelete, 'None', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,23 @@ export const sanitizerTransformer: Transformer = {
|
||||
ADD_TAGS: ['link', 'meta'],
|
||||
ADD_ATTR: (attributeName: string) => {
|
||||
return (
|
||||
['xmlns'].includes(attributeName) ||
|
||||
['xmlns', 'http-equiv', 'content', 'charset', 'link', 'vlink'].includes(attributeName) ||
|
||||
attributeName.startsWith('xml:') ||
|
||||
attributeName.startsWith('xmlns:') ||
|
||||
attributeName.startsWith('epub:')
|
||||
);
|
||||
},
|
||||
RETURN_DOM: true,
|
||||
});
|
||||
|
||||
sanitized = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html>' + sanitized;
|
||||
const serializer = new XMLSerializer();
|
||||
let serialized = serializer.serializeToString(sanitized);
|
||||
serialized = '<?xml version="1.0" encoding="utf-8"?>' + serialized;
|
||||
serialized = serialized.replace(/(<head[^>]*>)/i, '\n$1');
|
||||
serialized = serialized.replace(/(<\/body>)(<\/html>)/i, '$1\n$2');
|
||||
|
||||
// console.log(`Sanitizer diff:\n${diff(result, sanitized)}`);
|
||||
// console.log(`Sanitizer diff:\n${diff(result, serialized)}`);
|
||||
|
||||
return sanitized;
|
||||
return serialized;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getAPIBaseUrl } from '@/services/environment';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { ErrorCodes, TranslationProvider } from '../types';
|
||||
import { UserPlan } from '@/types/quota';
|
||||
import { getUserPlan } from '@/utils/access';
|
||||
import { getSubscriptionPlan } from '@/utils/access';
|
||||
import { normalizeToShortLang } from '@/utils/lang';
|
||||
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
|
||||
import { saveDailyUsage } from '../utils';
|
||||
@@ -29,7 +29,7 @@ export const deeplProvider: TranslationProvider = {
|
||||
|
||||
let userPlan: UserPlan = 'free';
|
||||
if (token) {
|
||||
userPlan = getUserPlan(token);
|
||||
userPlan = getSubscriptionPlan(token);
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export class TTSController extends EventTarget {
|
||||
ttsNativeVoices: TTSVoice[] = [];
|
||||
ttsTargetLang: string = '';
|
||||
|
||||
options: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
|
||||
|
||||
constructor(appService: AppService | null, view: FoliateView) {
|
||||
super();
|
||||
this.ttsWebClient = new WebSpeechClient(this);
|
||||
@@ -80,10 +82,10 @@ export class TTSController extends EventTarget {
|
||||
this.ttsEdgeVoices = await this.ttsEdgeClient.getAllVoices();
|
||||
}
|
||||
|
||||
#getHighlighter(options: TTSHighlightOptions) {
|
||||
#getHighlighter() {
|
||||
return (range: Range) => {
|
||||
const { overlayer } = this.view.renderer.getContents()[0] as { overlayer: Overlayer };
|
||||
const { style, color } = options;
|
||||
const { style, color } = this.options;
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
overlayer?.add(HIGHLIGHT_KEY, range, Overlayer[style], { color });
|
||||
};
|
||||
@@ -94,20 +96,23 @@ export class TTSController extends EventTarget {
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
}
|
||||
|
||||
async initViewTTS() {
|
||||
async initViewTTS(options?: TTSHighlightOptions) {
|
||||
if (options) {
|
||||
this.options.style = options.style;
|
||||
this.options.color = options.color;
|
||||
}
|
||||
let granularity: TTSGranularity = this.view.language.isCJK ? 'sentence' : 'word';
|
||||
const supportedGranularities = this.ttsClient.getGranularities();
|
||||
if (!supportedGranularities.includes(granularity)) {
|
||||
granularity = supportedGranularities[0]!;
|
||||
}
|
||||
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
|
||||
await this.view.initTTS(
|
||||
granularity,
|
||||
createRejectFilter({
|
||||
tags: ['rt', 'sup'],
|
||||
contents: [{ tag: 'a', content: /^\d+$/ }],
|
||||
}),
|
||||
this.#getHighlighter(highlightOptions),
|
||||
this.#getHighlighter(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type TTSGranularity = 'sentence' | 'word';
|
||||
|
||||
export type TTSHighlightOptions = {
|
||||
style: 'highlight' | 'underline' | 'squiggly' | 'outline';
|
||||
style: 'highlight' | 'underline' | 'strikethrough' | 'squiggly' | 'outline';
|
||||
color: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@ import { SUPPORTED_LANGNAMES } from '@/services/constants';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
import { useBookDataStore } from './bookDataStore';
|
||||
import { useLibraryStore } from './libraryStore';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
|
||||
interface ViewState {
|
||||
/* Unique key for each book view */
|
||||
key: string;
|
||||
view: FoliateView | null;
|
||||
viewerKey: string;
|
||||
isPrimary: boolean;
|
||||
loading: boolean;
|
||||
inited: boolean;
|
||||
@@ -72,12 +74,14 @@ interface ReaderStore {
|
||||
id: string,
|
||||
key: string,
|
||||
isPrimary?: boolean,
|
||||
reload?: boolean,
|
||||
) => Promise<void>;
|
||||
clearViewState: (key: string) => void;
|
||||
getViewState: (key: string) => ViewState | null;
|
||||
getGridInsets: (key: string) => Insets | null;
|
||||
setGridInsets: (key: string, insets: Insets | null) => void;
|
||||
setViewInited: (key: string, inited: boolean) => void;
|
||||
recreateViewer: (envConfig: EnvConfigType, key: string) => void;
|
||||
}
|
||||
|
||||
export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
@@ -110,7 +114,13 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
getViewState: (key: string) => get().viewStates[key] || null,
|
||||
initViewState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
initViewState: async (
|
||||
envConfig: EnvConfigType,
|
||||
id: string,
|
||||
key: string,
|
||||
isPrimary = true,
|
||||
reload = false,
|
||||
) => {
|
||||
const booksData = useBookDataStore.getState().booksData;
|
||||
const bookData = booksData[id];
|
||||
set((state) => ({
|
||||
@@ -119,6 +129,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
[key]: {
|
||||
key: '',
|
||||
view: null,
|
||||
viewerKey: '',
|
||||
isPrimary: false,
|
||||
loading: true,
|
||||
inited: false,
|
||||
@@ -134,7 +145,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}));
|
||||
try {
|
||||
const { settings } = useSettingsStore.getState();
|
||||
if (!bookData) {
|
||||
if (!bookData || reload) {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = useLibraryStore.getState();
|
||||
const book = library.find((b) => b.hash === id);
|
||||
@@ -183,6 +194,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
...state.viewStates[key],
|
||||
key,
|
||||
view: null,
|
||||
viewerKey: `${key}-${uniqueId()}`,
|
||||
isPrimary,
|
||||
loading: false,
|
||||
inited: false,
|
||||
@@ -205,6 +217,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
...state.viewStates[key],
|
||||
key: '',
|
||||
view: null,
|
||||
viewerKey: '',
|
||||
isPrimary: false,
|
||||
loading: false,
|
||||
inited: false,
|
||||
@@ -378,4 +391,21 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
recreateViewer: (envConfig: EnvConfigType, key: string) => {
|
||||
const id = key.split('-')[0]!;
|
||||
get()
|
||||
.initViewState(envConfig, id, key, true, true)
|
||||
.then(() => {
|
||||
set((state) => ({
|
||||
viewStates: {
|
||||
...state.viewStates,
|
||||
[key]: {
|
||||
...state.viewStates[key]!,
|
||||
viewerKey: `${key}-${uniqueId()}`,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -412,6 +412,10 @@ foliate-view {
|
||||
animation: spin-counterclockwise 1s linear;
|
||||
}
|
||||
|
||||
[data-eink="true"] [class*="shadow-"] {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.no-transitions * {
|
||||
transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { TTSHighlightOptions } from '@/services/tts/types';
|
||||
|
||||
export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'AZW' | 'AZW3' | 'CBZ' | 'FB2' | 'FBZ';
|
||||
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
|
||||
@@ -175,6 +176,7 @@ export interface TTSConfig {
|
||||
ttsVoice: string;
|
||||
ttsLocation: string;
|
||||
showTTSBar: boolean;
|
||||
ttsHighlightOptions: TTSHighlightOptions;
|
||||
}
|
||||
|
||||
export interface TranslatorConfig {
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ReadSettings {
|
||||
highlightStyle: HighlightStyle;
|
||||
highlightStyles: Record<HighlightStyle, HighlightColor>;
|
||||
customHighlightColors: Record<HighlightColor, string>;
|
||||
customTtsHighlightColors: string[];
|
||||
customThemes: CustomTheme[];
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ export interface SystemSettings {
|
||||
openLastBooks: boolean;
|
||||
lastOpenBooks: string[];
|
||||
autoImportBooksOnOpen: boolean;
|
||||
savedBookCoverForLockScreen: string;
|
||||
telemetryEnabled: boolean;
|
||||
libraryViewMode: LibraryViewModeType;
|
||||
librarySortBy: LibrarySortByType;
|
||||
@@ -69,6 +71,8 @@ export interface SystemSettings {
|
||||
lastSyncedAtConfigs: number;
|
||||
lastSyncedAtNotes: number;
|
||||
|
||||
migrationVersion: number;
|
||||
|
||||
globalReadSettings: ReadSettings;
|
||||
globalViewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,23 @@ interface Token {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
export const getUserPlan = (token: string): UserPlan => {
|
||||
export const getSubscriptionPlan = (token: string): UserPlan => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
return data['plan'] || 'free';
|
||||
};
|
||||
|
||||
export const getUserProfilePlan = (token: string): UserPlan => {
|
||||
const data = jwtDecode<Token>(token) || {};
|
||||
let plan = data['plan'] || 'free';
|
||||
if (plan === 'free') {
|
||||
const purchasedQuota = data['storage_purchased_bytes'] || 0;
|
||||
if (purchasedQuota > 0) {
|
||||
plan = 'purchase';
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
};
|
||||
|
||||
export const STORAGE_QUOTA_GRACE_BYTES = 10 * 1024 * 1024; // 10 MB grace
|
||||
|
||||
export const getStoragePlanData = (token: string) => {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
export function diff(str1: string, str2: string) {
|
||||
const lines1 = str1.split('\n');
|
||||
const lines2 = str2.split('\n');
|
||||
const lines1 = str1.split('\n').filter((line) => line.trim() !== '');
|
||||
const lines2 = str2.split('\n').filter((line) => line.trim() !== '');
|
||||
|
||||
const lcs = longestCommonSubsequence(lines1, lines2);
|
||||
|
||||
@@ -17,17 +17,17 @@ export function diff(str1: string, str2: string) {
|
||||
const addRange = (start: number, end: number) => (start === end ? `${start}` : `${start},${end}`);
|
||||
|
||||
while (i < lines1.length || j < lines2.length) {
|
||||
if (k < lcs.length && i < lines1.length && lines1[i] === lcs[k]) {
|
||||
if (k < lcs.length && i < lines1.length && trimCompare(lines1[i], lcs[k])) {
|
||||
// common line
|
||||
i++;
|
||||
j++;
|
||||
k++;
|
||||
} else {
|
||||
let delStart = i,
|
||||
addStart = j;
|
||||
let delStart = i;
|
||||
let addStart = j;
|
||||
|
||||
while (i < lines1.length && (k >= lcs.length || lines1[i] !== lcs[k])) i++;
|
||||
while (j < lines2.length && (k >= lcs.length || lines2[j] !== lcs[k])) j++;
|
||||
while (i < lines1.length && (k >= lcs.length || !trimCompare(lines1[i], lcs[k]))) i++;
|
||||
while (j < lines2.length && (k >= lcs.length || !trimCompare(lines2[j], lcs[k]))) j++;
|
||||
|
||||
const delRange = addRange(delStart + 1, i);
|
||||
const addRangeStr = addRange(addStart + 1, j);
|
||||
@@ -53,6 +53,10 @@ export function diff(str1: string, str2: string) {
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
function trimCompare(a: string | undefined, b: string | undefined) {
|
||||
return (a || '').trim() === (b || '').trim();
|
||||
}
|
||||
|
||||
function longestCommonSubsequence(arr1: string[], arr2: string[]) {
|
||||
const m = arr1.length;
|
||||
const n = arr2.length;
|
||||
@@ -62,7 +66,7 @@ function longestCommonSubsequence(arr1: string[], arr2: string[]) {
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (arr1[i - 1] === arr2[j - 1]) {
|
||||
if (trimCompare(arr1[i - 1], arr2[j - 1])) {
|
||||
dp[i]![j] = dp[i - 1]![j - 1] + 1;
|
||||
} else {
|
||||
dp[i]![j] = Math.max(dp[i - 1]![j], dp[i]![j - 1]);
|
||||
@@ -75,7 +79,7 @@ function longestCommonSubsequence(arr1: string[], arr2: string[]) {
|
||||
j = n;
|
||||
|
||||
while (i > 0 && j > 0) {
|
||||
if (arr1[i - 1] === arr2[j - 1]) {
|
||||
if (trimCompare(arr1[i - 1], arr2[j - 1])) {
|
||||
lcs.unshift(arr1[i - 1]!);
|
||||
i--;
|
||||
j--;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { AppService, FileItem } from '@/types/system';
|
||||
|
||||
export const copyFiles = async (appService: AppService, srcDir: string, dstDir: string) => {
|
||||
let filesToCopy: FileItem[] = [];
|
||||
try {
|
||||
filesToCopy = await appService.readDirectory(srcDir, 'None');
|
||||
} catch (error) {
|
||||
throw new Error(`Dir ${srcDir} failed to read.`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < filesToCopy.length; i++) {
|
||||
const file = filesToCopy[i]!;
|
||||
const srcPath = `${srcDir}/${file.path}`;
|
||||
const destPath = `${dstDir}/${file.path}`;
|
||||
await appService.copyFile(srcPath, destPath, 'None');
|
||||
}
|
||||
|
||||
const filesCopied = await appService.readDirectory(dstDir, 'None');
|
||||
for (const file of filesToCopy) {
|
||||
if (!filesCopied.find((f) => f.path === file.path && f.size === file.size)) {
|
||||
throw new Error(`File ${file.path} failed to copy.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { RELOAD_BEFORE_SAVED_TIMEOUT_MS } from '@/services/constants';
|
||||
|
||||
export const saveAndReload = async () => {
|
||||
setTimeout(() => window.location.reload(), RELOAD_BEFORE_SAVED_TIMEOUT_MS);
|
||||
};
|
||||
@@ -391,6 +391,8 @@ export const getFootnoteStyles = () => `
|
||||
}
|
||||
|
||||
a:any-link {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
padding: unset;
|
||||
margin: unset;
|
||||
|
||||
Reference in New Issue
Block a user