feat(library): add Purge Data and fold detail actions into a More menu (#4615) (#4626)

Resolves #4615. Re-importing updated serials left the app-generated
Books/<hash>/ folder (config.json reading progress/notes, nav.json, cover)
on disk after a normal delete, forcing a manual cleanup. "Purge Data" now
does a Cloud & Device delete AND wipes the whole directory in one action.

The book detail action row is redesigned to Edit · Download · Upload ·
Delete · More (hamburger):
- Goodreads, Share, and Export move into the hamburger "More" menu.
- Share is enabled only when signed in and the local file exists; Export
  is enabled when the local file exists (kept on every platform since the
  bottom-bar Send is mobile/macOS-only).
- Purge Data is the red entry in the Delete menu, behind a strong confirm.

Implementation:
- DeleteAction gains 'purge'; cloudService.deleteBook('purge') removes the
  in-place source file and removeDir's the whole Books/<hash>/ folder,
  clearing downloadedAt and leaving the tombstone + queued cloud delete to
  the page (mirrors 'both'/'local').
- The library page wires handleBookDelete('purge'); BookDetailModal adds
  the purge confirm config + share/export handlers and gates Share on auth.

Tests: cloud-service purge cases, BookDetailView More-menu + Purge tests.
i18n: 9 new keys translated across all 33 locales.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-17 12:28:21 +08:00
committed by GitHub
parent 5e217544f2
commit d5c02e6253
41 changed files with 630 additions and 76 deletions
@@ -1889,5 +1889,14 @@
"Clear Pending": "مسح قيد الانتظار",
"Reading statistics": "إحصائيات القراءة",
"Reading time and pages read, synced across your devices and KOReader.": "وقت القراءة والصفحات المقروءة، تتم مزامنتها عبر أجهزتك و KOReader.",
"Auto Sync": "مزامنة تلقائية"
"Auto Sync": "مزامنة تلقائية",
"Purged book data: {{title}}": "تم محو بيانات الكتاب: {{title}}",
"Failed to purge book data: {{title}}": "فشل محو بيانات الكتاب: {{title}}",
"Purge Book Data": "محو جميع بيانات الكتاب",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "سيؤدي هذا إلى محو الكتاب وجميع بياناته نهائيًا، بما في ذلك تقدم القراءة والملاحظات والإشارات المرجعية. لا يمكن التراجع عن هذا الإجراء.",
"Purge Data": "محو جميع البيانات",
"Erase the book and all its data, including reading progress": "محو الكتاب وجميع بياناته، بما في ذلك تقدم القراءة",
"More Actions": "إجراءات أخرى",
"Sign in and make the book available to share it": "سجّل الدخول واجعل الكتاب متاحًا لمشاركته",
"Download the book to export it": "نزّل الكتاب لتصديره"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "অপেক্ষমাণ মুছুন",
"Reading statistics": "পড়ার পরিসংখ্যান",
"Reading time and pages read, synced across your devices and KOReader.": "পড়ার সময় এবং পড়া পৃষ্ঠা, আপনার ডিভাইস এবং KOReader জুড়ে সিঙ্ক করা হয়।",
"Auto Sync": "স্বয়ংক্রিয় সিঙ্ক"
"Auto Sync": "স্বয়ংক্রিয় সিঙ্ক",
"Purged book data: {{title}}": "বইয়ের ডেটা মুছে ফেলা হয়েছে: {{title}}",
"Failed to purge book data: {{title}}": "বইয়ের ডেটা মুছতে ব্যর্থ: {{title}}",
"Purge Book Data": "বইয়ের সমস্ত ডেটা মুছুন",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "এটি বইটি এবং পড়ার অগ্রগতি, নোট ও বুকমার্ক সহ এর সমস্ত ডেটা স্থায়ীভাবে মুছে ফেলবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
"Purge Data": "সমস্ত ডেটা মুছুন",
"Erase the book and all its data, including reading progress": "পড়ার অগ্রগতিসহ বই ও এর সমস্ত ডেটা মুছে ফেলুন",
"More Actions": "আরও ক্রিয়া",
"Sign in and make the book available to share it": "শেয়ার করতে সাইন ইন করুন এবং বইটি উপলব্ধ করুন",
"Download the book to export it": "এক্সপোর্ট করতে বইটি ডাউনলোড করুন"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "སྒུག་བཞིན་པ་གསལ་བ་",
"Reading statistics": "ཀློག་འདོན་གྱི་གྲངས་ཐོ།",
"Reading time and pages read, synced across your devices and KOReader.": "ཀློག་འདོན་གྱི་དུས་ཚོད་དང་ཀློག་ཟིན་པའི་ཤོག་གྲངས། ཁྱེད་ཀྱི་སྒྲིག་ཆས་དང་ KOReader བར་མཉམ་སྒྲིག་བྱེད།",
"Auto Sync": "རང་འགུལ་སྤྲི་དོན།"
"Auto Sync": "རང་འགུལ་སྤྲི་དོན།",
"Purged book data: {{title}}": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་བསུབ་ཟིན། {{title}}",
"Failed to purge book data: {{title}}": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་བསུབ་མ་ཐུབ། {{title}}",
"Purge Book Data": "དཔེ་དེབ་ཀྱི་གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "འདིས་དཔེ་དེབ་དང་ཀློག་འདོན་གྱི་འཕེལ་རིམ། ཟིན་ཐོ། དེབ་རྟགས་བཅས་ཀྱི་གཞི་གྲངས་ཡོངས་རྫོགས་གཏན་འཁེལ་དུ་བསུབ་པ་ཡིན། འདི་ཕྱིར་བསྒྱུར་མི་ཐུབ།",
"Purge Data": "གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"Erase the book and all its data, including reading progress": "དཔེ་དེབ་དང་ཀློག་འདོན་གྱི་འཕེལ་རིམ་བཅས་ཀྱི་གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
"More Actions": "བྱ་སྤྱོད་གཞན་དག",
"Sign in and make the book available to share it": "ནང་འཛུལ་བྱས་ནས་དཔེ་དེབ་མཁོ་སྤྲོད་རུང་བར་བཟོས་ནས་མཉམ་སྤྱོད་བྱོས།",
"Download the book to export it": "ཕྱིར་འདྲེན་བྱེད་པར་དཔེ་དེབ་ཕབ་ལེན་བྱོས།"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Ausstehende löschen",
"Reading statistics": "Lesestatistik",
"Reading time and pages read, synced across your devices and KOReader.": "Lesezeit und gelesene Seiten, synchronisiert über deine Geräte und KOReader.",
"Auto Sync": "Automatische Synchronisierung"
"Auto Sync": "Automatische Synchronisierung",
"Purged book data: {{title}}": "Buchdaten gelöscht: {{title}}",
"Failed to purge book data: {{title}}": "Buchdaten konnten nicht gelöscht werden: {{title}}",
"Purge Book Data": "Buchdaten endgültig löschen",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Dadurch werden das Buch und alle zugehörigen Daten einschließlich Lesefortschritt, Notizen und Lesezeichen endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.",
"Purge Data": "Alle Daten löschen",
"Erase the book and all its data, including reading progress": "Buch und alle Daten löschen, einschließlich Lesefortschritt",
"More Actions": "Weitere Aktionen",
"Sign in and make the book available to share it": "Melde dich an und mache das Buch verfügbar, um es zu teilen",
"Download the book to export it": "Lade das Buch herunter, um es zu exportieren"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Εκκαθάριση σε αναμονή",
"Reading statistics": "Στατιστικά ανάγνωσης",
"Reading time and pages read, synced across your devices and KOReader.": "Χρόνος ανάγνωσης και σελίδες που διαβάστηκαν, συγχρονισμένα στις συσκευές σας και το KOReader.",
"Auto Sync": "Αυτόματος συγχρονισμός"
"Auto Sync": "Αυτόματος συγχρονισμός",
"Purged book data: {{title}}": "Τα δεδομένα του βιβλίου διαγράφηκαν: {{title}}",
"Failed to purge book data: {{title}}": "Αποτυχία διαγραφής δεδομένων βιβλίου: {{title}}",
"Purge Book Data": "Διαγραφή δεδομένων βιβλίου",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Αυτό διαγράφει οριστικά το βιβλίο και όλα τα δεδομένα του, συμπεριλαμβανομένης της προόδου ανάγνωσης, των σημειώσεων και των σελιδοδεικτών. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"Purge Data": "Διαγραφή όλων των δεδομένων",
"Erase the book and all its data, including reading progress": "Διαγράφει το βιβλίο και όλα τα δεδομένα του, συμπεριλαμβανομένης της προόδου ανάγνωσης",
"More Actions": "Περισσότερες ενέργειες",
"Sign in and make the book available to share it": "Συνδεθείτε και κάντε το βιβλίο διαθέσιμο για να το μοιραστείτε",
"Download the book to export it": "Κατεβάστε το βιβλίο για να το εξαγάγετε"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Limpiar pendientes",
"Reading statistics": "Estadísticas de lectura",
"Reading time and pages read, synced across your devices and KOReader.": "Tiempo de lectura y páginas leídas, sincronizados entre tus dispositivos y KOReader.",
"Auto Sync": "Sincronización automática"
"Auto Sync": "Sincronización automática",
"Purged book data: {{title}}": "Datos del libro borrados: {{title}}",
"Failed to purge book data: {{title}}": "No se pudieron borrar los datos del libro: {{title}}",
"Purge Book Data": "Borrar todos los datos del libro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Esto borra permanentemente el libro y todos sus datos, incluido el progreso de lectura, las notas y los marcadores. Esta acción no se puede deshacer.",
"Purge Data": "Borrar todos los datos",
"Erase the book and all its data, including reading progress": "Borra el libro y todos sus datos, incluido el progreso de lectura",
"More Actions": "Más acciones",
"Sign in and make the book available to share it": "Inicia sesión y ten el libro disponible para compartirlo",
"Download the book to export it": "Descarga el libro para exportarlo"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "پاک کردن در انتظارها",
"Reading statistics": "آمار مطالعه",
"Reading time and pages read, synced across your devices and KOReader.": "زمان مطالعه و صفحات خوانده‌شده، همگام‌سازی‌شده در دستگاه‌های شما و KOReader.",
"Auto Sync": "همگام‌سازی خودکار"
"Auto Sync": "همگام‌سازی خودکار",
"Purged book data: {{title}}": "داده‌های کتاب پاک شد: {{title}}",
"Failed to purge book data: {{title}}": "پاک‌سازی داده‌های کتاب ناموفق بود: {{title}}",
"Purge Book Data": "پاک‌سازی داده‌های کتاب",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "این کار کتاب و تمام داده‌های آن، شامل پیشرفت مطالعه، یادداشت‌ها و نشانک‌ها را برای همیشه پاک می‌کند. این عمل قابل بازگشت نیست.",
"Purge Data": "پاک‌سازی داده‌ها",
"Erase the book and all its data, including reading progress": "کتاب و تمام داده‌های آن، شامل پیشرفت مطالعه را پاک می‌کند",
"More Actions": "اقدامات بیشتر",
"Sign in and make the book available to share it": "برای اشتراک‌گذاری، وارد شوید و کتاب را در دسترس قرار دهید",
"Download the book to export it": "برای خروجی گرفتن، کتاب را دانلود کنید"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Effacer les en attente",
"Reading statistics": "Statistiques de lecture",
"Reading time and pages read, synced across your devices and KOReader.": "Temps de lecture et pages lues, synchronisés sur vos appareils et KOReader.",
"Auto Sync": "Synchronisation automatique"
"Auto Sync": "Synchronisation automatique",
"Purged book data: {{title}}": "Données du livre purgées : {{title}}",
"Failed to purge book data: {{title}}": "Échec de la purge des données du livre : {{title}}",
"Purge Book Data": "Purger les données du livre",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Cette action supprime définitivement le livre et toutes ses données, y compris la progression de lecture, les notes et les signets. Cette action est irréversible.",
"Purge Data": "Purger les données",
"Erase the book and all its data, including reading progress": "Supprime le livre et toutes ses données, y compris la progression de lecture",
"More Actions": "Plus d'actions",
"Sign in and make the book available to share it": "Connectez-vous et rendez le livre disponible pour le partager",
"Download the book to export it": "Téléchargez le livre pour l'exporter"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "נקה בהמתנה",
"Reading statistics": "סטטיסטיקת קריאה",
"Reading time and pages read, synced across your devices and KOReader.": "זמן קריאה ועמודים שנקראו, מסונכרנים בין המכשירים שלך ו-KOReader.",
"Auto Sync": "סנכרון אוטומטי"
"Auto Sync": "סנכרון אוטומטי",
"Purged book data: {{title}}": "נתוני הספר נמחקו: {{title}}",
"Failed to purge book data: {{title}}": "מחיקת נתוני הספר נכשלה: {{title}}",
"Purge Book Data": "מחיקת כל נתוני הספר",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "פעולה זו מוחקת לצמיתות את הספר ואת כל הנתונים שלו, כולל התקדמות הקריאה, ההערות והסימניות. לא ניתן לבטל פעולה זו.",
"Purge Data": "מחק את כל הנתונים",
"Erase the book and all its data, including reading progress": "מחיקת הספר וכל הנתונים שלו, כולל התקדמות הקריאה",
"More Actions": "פעולות נוספות",
"Sign in and make the book available to share it": "היכנס והפוך את הספר לזמין כדי לשתף אותו",
"Download the book to export it": "הורד את הספר כדי לייצא אותו"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "लंबित साफ़ करें",
"Reading statistics": "पठन सांख्यिकी",
"Reading time and pages read, synced across your devices and KOReader.": "पढ़ने का समय और पढ़े गए पृष्ठ, आपके डिवाइसों और KOReader के बीच सिंक किए गए।",
"Auto Sync": "स्वतः सिंक"
"Auto Sync": "स्वतः सिंक",
"Purged book data: {{title}}": "पुस्तक का डेटा मिटाया गया: {{title}}",
"Failed to purge book data: {{title}}": "पुस्तक का डेटा मिटाने में विफल: {{title}}",
"Purge Book Data": "पुस्तक का सारा डेटा मिटाएँ",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "इससे पुस्तक और उसका सारा डेटा, जिसमें पढ़ने की प्रगति, नोट्स और बुकमार्क शामिल हैं, स्थायी रूप से मिट जाएगा। इसे पूर्ववत नहीं किया जा सकता।",
"Purge Data": "सारा डेटा मिटाएँ",
"Erase the book and all its data, including reading progress": "पढ़ने की प्रगति सहित पुस्तक और उसका सारा डेटा मिटाएँ",
"More Actions": "अधिक क्रियाएँ",
"Sign in and make the book available to share it": "साझा करने के लिए साइन इन करें और पुस्तक उपलब्ध कराएँ",
"Download the book to export it": "निर्यात करने के लिए पुस्तक डाउनलोड करें"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Függőben lévők törlése",
"Reading statistics": "Olvasási statisztika",
"Reading time and pages read, synced across your devices and KOReader.": "Olvasási idő és elolvasott oldalak, szinkronizálva az eszközei és a KOReader között.",
"Auto Sync": "Automatikus szinkronizálás"
"Auto Sync": "Automatikus szinkronizálás",
"Purged book data: {{title}}": "Könyv adatai törölve: {{title}}",
"Failed to purge book data: {{title}}": "A könyv adatainak törlése sikertelen: {{title}}",
"Purge Book Data": "Könyv összes adatának törlése",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ez véglegesen törli a könyvet és minden adatát, beleértve az olvasási előrehaladást, a jegyzeteket és a könyvjelzőket. A művelet nem vonható vissza.",
"Purge Data": "Összes adat törlése",
"Erase the book and all its data, including reading progress": "Törli a könyvet és minden adatát, beleértve az olvasási előrehaladást",
"More Actions": "További műveletek",
"Sign in and make the book available to share it": "Jelentkezz be, és tedd elérhetővé a könyvet a megosztáshoz",
"Download the book to export it": "Töltsd le a könyvet az exportáláshoz"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "Hapus Menunggu",
"Reading statistics": "Statistik Bacaan",
"Reading time and pages read, synced across your devices and KOReader.": "Waktu membaca dan halaman yang dibaca, disinkronkan di seluruh perangkat Anda dan KOReader.",
"Auto Sync": "Sinkronisasi Otomatis"
"Auto Sync": "Sinkronisasi Otomatis",
"Purged book data: {{title}}": "Data buku dihapus: {{title}}",
"Failed to purge book data: {{title}}": "Gagal menghapus data buku: {{title}}",
"Purge Book Data": "Hapus semua data buku",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ini akan menghapus buku secara permanen beserta semua datanya, termasuk progres baca, catatan, dan markah. Tindakan ini tidak dapat dibatalkan.",
"Purge Data": "Hapus semua data",
"Erase the book and all its data, including reading progress": "Menghapus buku dan semua datanya, termasuk progres baca",
"More Actions": "Tindakan lainnya",
"Sign in and make the book available to share it": "Masuk dan sediakan buku untuk membagikannya",
"Download the book to export it": "Unduh buku untuk mengekspornya"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Cancella in attesa",
"Reading statistics": "Statistiche di lettura",
"Reading time and pages read, synced across your devices and KOReader.": "Tempo di lettura e pagine lette, sincronizzati tra i tuoi dispositivi e KOReader.",
"Auto Sync": "Sincronizzazione automatica"
"Auto Sync": "Sincronizzazione automatica",
"Purged book data: {{title}}": "Dati del libro eliminati: {{title}}",
"Failed to purge book data: {{title}}": "Impossibile eliminare i dati del libro: {{title}}",
"Purge Book Data": "Elimina tutti i dati del libro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Questa operazione elimina definitivamente il libro e tutti i suoi dati, inclusi i progressi di lettura, le note e i segnalibri. L'operazione non può essere annullata.",
"Purge Data": "Elimina tutti i dati",
"Erase the book and all its data, including reading progress": "Elimina il libro e tutti i suoi dati, inclusi i progressi di lettura",
"More Actions": "Altre azioni",
"Sign in and make the book available to share it": "Accedi e rendi disponibile il libro per condividerlo",
"Download the book to export it": "Scarica il libro per esportarlo"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "待機中を消去",
"Reading statistics": "読書統計",
"Reading time and pages read, synced across your devices and KOReader.": "読書時間と読んだページ数。デバイス間および KOReader と同期されます。",
"Auto Sync": "自動同期"
"Auto Sync": "自動同期",
"Purged book data: {{title}}": "書籍データを消去しました: {{title}}",
"Failed to purge book data: {{title}}": "書籍データの消去に失敗しました: {{title}}",
"Purge Book Data": "書籍データを消去",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "これにより、本と読書の進行状況・メモ・しおりを含むすべてのデータが完全に削除されます。この操作は取り消せません。",
"Purge Data": "データを消去",
"Erase the book and all its data, including reading progress": "本と読書の進行状況を含むすべてのデータを消去します",
"More Actions": "その他の操作",
"Sign in and make the book available to share it": "共有するにはサインインして書籍を利用可能にしてください",
"Download the book to export it": "エクスポートするには書籍をダウンロードしてください"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "대기 중 항목 지우기",
"Reading statistics": "독서 통계",
"Reading time and pages read, synced across your devices and KOReader.": "읽은 시간과 읽은 페이지 수로, 여러 기기와 KOReader 간에 동기화됩니다.",
"Auto Sync": "자동 동기화"
"Auto Sync": "자동 동기화",
"Purged book data: {{title}}": "도서 데이터를 삭제했습니다: {{title}}",
"Failed to purge book data: {{title}}": "도서 데이터 삭제에 실패했습니다: {{title}}",
"Purge Book Data": "도서 데이터 완전 삭제",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "책과 읽기 진행률, 메모, 책갈피를 포함한 모든 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
"Purge Data": "데이터 완전 삭제",
"Erase the book and all its data, including reading progress": "읽기 진행률을 포함한 책과 모든 데이터를 삭제합니다",
"More Actions": "추가 작업",
"Sign in and make the book available to share it": "공유하려면 로그인하고 책을 사용할 수 있도록 하세요",
"Download the book to export it": "내보내려면 책을 다운로드하세요"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "Kosongkan Menunggu",
"Reading statistics": "Statistik Bacaan",
"Reading time and pages read, synced across your devices and KOReader.": "Masa membaca dan halaman dibaca, disegerakkan merentas peranti anda dan KOReader.",
"Auto Sync": "Segerak Automatik"
"Auto Sync": "Segerak Automatik",
"Purged book data: {{title}}": "Data buku dipadam: {{title}}",
"Failed to purge book data: {{title}}": "Gagal memadam data buku: {{title}}",
"Purge Book Data": "Padam semua data buku",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Ini memadamkan buku dan semua datanya secara kekal, termasuk kemajuan bacaan, nota dan penanda buku. Tindakan ini tidak boleh dibuat asal.",
"Purge Data": "Padam semua data",
"Erase the book and all its data, including reading progress": "Memadam buku dan semua datanya, termasuk kemajuan bacaan",
"More Actions": "Tindakan lain",
"Sign in and make the book available to share it": "Log masuk dan sediakan buku untuk berkongsinya",
"Download the book to export it": "Muat turun buku untuk mengeksportnya"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Wachtende wissen",
"Reading statistics": "Leesstatistieken",
"Reading time and pages read, synced across your devices and KOReader.": "Leestijd en gelezen pagina's, gesynchroniseerd op je apparaten en KOReader.",
"Auto Sync": "Automatische synchronisatie"
"Auto Sync": "Automatische synchronisatie",
"Purged book data: {{title}}": "Boekgegevens gewist: {{title}}",
"Failed to purge book data: {{title}}": "Kan boekgegevens niet wissen: {{title}}",
"Purge Book Data": "Alle boekgegevens wissen",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Hiermee worden het boek en alle bijbehorende gegevens, waaronder leesvoortgang, notities en bladwijzers, permanent gewist. Dit kan niet ongedaan worden gemaakt.",
"Purge Data": "Alle gegevens wissen",
"Erase the book and all its data, including reading progress": "Wist het boek en alle gegevens, inclusief leesvoortgang",
"More Actions": "Meer acties",
"Sign in and make the book available to share it": "Meld je aan en maak het boek beschikbaar om het te delen",
"Download the book to export it": "Download het boek om het te exporteren"
}
@@ -1823,5 +1823,14 @@
"Clear Pending": "Wyczyść oczekujące",
"Reading statistics": "Statystyki czytania",
"Reading time and pages read, synced across your devices and KOReader.": "Czas czytania i przeczytane strony, synchronizowane między Twoimi urządzeniami a KOReader.",
"Auto Sync": "Automatyczna synchronizacja"
"Auto Sync": "Automatyczna synchronizacja",
"Purged book data: {{title}}": "Usunięto dane książki: {{title}}",
"Failed to purge book data: {{title}}": "Nie udało się usunąć danych książki: {{title}}",
"Purge Book Data": "Usuń wszystkie dane książki",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Spowoduje to trwałe usunięcie książki i wszystkich jej danych, w tym postępu czytania, notatek i zakładek. Tej operacji nie można cofnąć.",
"Purge Data": "Usuń wszystkie dane",
"Erase the book and all its data, including reading progress": "Usuwa książkę i wszystkie jej dane, w tym postęp czytania",
"More Actions": "Więcej akcji",
"Sign in and make the book available to share it": "Zaloguj się i pobierz książkę, aby ją udostępnić",
"Download the book to export it": "Pobierz książkę, aby ją wyeksportować"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Limpar pendentes",
"Reading statistics": "Estatísticas de leitura",
"Reading time and pages read, synced across your devices and KOReader.": "Tempo de leitura e páginas lidas, sincronizados entre seus dispositivos e o KOReader.",
"Auto Sync": "Sincronização automática"
"Auto Sync": "Sincronização automática",
"Purged book data: {{title}}": "Dados do livro apagados: {{title}}",
"Failed to purge book data: {{title}}": "Falha ao apagar os dados do livro: {{title}}",
"Purge Book Data": "Apagar todos os dados do livro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Isso apaga permanentemente o livro e todos os seus dados, incluindo o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Purge Data": "Apagar todos os dados",
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
"More Actions": "Mais ações",
"Sign in and make the book available to share it": "Faça login e tenha o livro disponível para compartilhá-lo",
"Download the book to export it": "Baixe o livro para exportá-lo"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Limpar Pendentes",
"Reading statistics": "Estatísticas de leitura",
"Reading time and pages read, synced across your devices and KOReader.": "Tempo de leitura e páginas lidas, sincronizados entre os seus dispositivos e o KOReader.",
"Auto Sync": "Sincronização Automática"
"Auto Sync": "Sincronização Automática",
"Purged book data: {{title}}": "Dados do livro apagados: {{title}}",
"Failed to purge book data: {{title}}": "Falha ao apagar os dados do livro: {{title}}",
"Purge Book Data": "Apagar todos os dados do livro",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Isto apaga permanentemente o livro e todos os seus dados, incluindo o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Purge Data": "Apagar todos os dados",
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
"More Actions": "Mais ações",
"Sign in and make the book available to share it": "Inicie sessão e tenha o livro disponível para o partilhar",
"Download the book to export it": "Transfira o livro para o exportar"
}
@@ -1790,5 +1790,14 @@
"Clear Pending": "Șterge în așteptare",
"Reading statistics": "Statistici de lectură",
"Reading time and pages read, synced across your devices and KOReader.": "Timpul de citire și paginile citite, sincronizate pe dispozitivele tale și KOReader.",
"Auto Sync": "Sincronizare automată"
"Auto Sync": "Sincronizare automată",
"Purged book data: {{title}}": "Datele cărții au fost șterse: {{title}}",
"Failed to purge book data: {{title}}": "Ștergerea datelor cărții a eșuat: {{title}}",
"Purge Book Data": "Șterge toate datele cărții",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Aceasta șterge definitiv cartea și toate datele sale, inclusiv progresul lecturii, notițele și marcajele. Această acțiune nu poate fi anulată.",
"Purge Data": "Șterge toate datele",
"Erase the book and all its data, including reading progress": "Șterge cartea și toate datele sale, inclusiv progresul lecturii",
"More Actions": "Mai multe acțiuni",
"Sign in and make the book available to share it": "Conectează-te și fă cartea disponibilă pentru a o partaja",
"Download the book to export it": "Descarcă cartea pentru a o exporta"
}
@@ -1823,5 +1823,14 @@
"Clear Pending": "Очистить ожидающие",
"Reading statistics": "Статистика чтения",
"Reading time and pages read, synced across your devices and KOReader.": "Время чтения и прочитанные страницы, синхронизируются между вашими устройствами и KOReader.",
"Auto Sync": "Автосинхронизация"
"Auto Sync": "Автосинхронизация",
"Purged book data: {{title}}": "Данные книги очищены: {{title}}",
"Failed to purge book data: {{title}}": "Не удалось очистить данные книги: {{title}}",
"Purge Book Data": "Очистить данные книги",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Это навсегда удалит книгу и все её данные, включая прогресс чтения, заметки и закладки. Это действие нельзя отменить.",
"Purge Data": "Очистить данные",
"Erase the book and all its data, including reading progress": "Удаляет книгу и все её данные, включая прогресс чтения",
"More Actions": "Ещё действия",
"Sign in and make the book available to share it": "Войдите и сделайте книгу доступной, чтобы поделиться ею",
"Download the book to export it": "Скачайте книгу, чтобы экспортировать её"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "බලාපොරොත්තුවේ ඇති ඒවා මකන්න",
"Reading statistics": "කියවීමේ සංඛ්‍යාලේඛන",
"Reading time and pages read, synced across your devices and KOReader.": "කියවීමේ කාලය සහ කියවූ පිටු, ඔබේ උපාංග සහ KOReader හරහා සමමුහුර්ත වේ.",
"Auto Sync": "ස්වයංක්‍රීය සමමුහුර්තය"
"Auto Sync": "ස්වයංක්‍රීය සමමුහුර්තය",
"Purged book data: {{title}}": "පොතේ දත්ත මකා දමන ලදී: {{title}}",
"Failed to purge book data: {{title}}": "පොතේ දත්ත මැකීමට අසමත් විය: {{title}}",
"Purge Book Data": "පොතේ සියලු දත්ත මකන්න",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "මෙය පොත සහ කියවීමේ ප්‍රගතිය, සටහන් සහ පිටු සලකුණු ඇතුළුව එහි සියලු දත්ත ස්ථිරවම මකා දමයි. මෙය පෙරළිය නොහැක.",
"Purge Data": "සියලු දත්ත මකන්න",
"Erase the book and all its data, including reading progress": "කියවීමේ ප්‍රගතිය ඇතුළුව පොත සහ එහි සියලු දත්ත මකන්න",
"More Actions": "තවත් ක්‍රියා",
"Sign in and make the book available to share it": "බෙදා ගැනීමට පුරනය වී පොත ලබා ගත හැකි කරන්න",
"Download the book to export it": "අපනයනය කිරීමට පොත බාගන්න"
}
@@ -1823,5 +1823,14 @@
"Clear Pending": "Počisti čakajoče",
"Reading statistics": "Statistika branja",
"Reading time and pages read, synced across your devices and KOReader.": "Čas branja in prebrane strani, sinhronizirano med vašimi napravami in KOReader.",
"Auto Sync": "Samodejna sinhronizacija"
"Auto Sync": "Samodejna sinhronizacija",
"Purged book data: {{title}}": "Podatki knjige izbrisani: {{title}}",
"Failed to purge book data: {{title}}": "Podatkov knjige ni bilo mogoče izbrisati: {{title}}",
"Purge Book Data": "Izbriši vse podatke knjige",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "S tem trajno izbrišete knjigo in vse njene podatke, vključno z napredkom branja, zapiski in zaznamki. Tega dejanja ni mogoče razveljaviti.",
"Purge Data": "Izbriši vse podatke",
"Erase the book and all its data, including reading progress": "Izbriše knjigo in vse njene podatke, vključno z napredkom branja",
"More Actions": "Več dejanj",
"Sign in and make the book available to share it": "Prijavite se in omogočite knjigo za deljenje",
"Download the book to export it": "Prenesite knjigo, da jo izvozite"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Rensa väntande",
"Reading statistics": "Lässtatistik",
"Reading time and pages read, synced across your devices and KOReader.": "Lästid och lästa sidor, synkroniserat mellan dina enheter och KOReader.",
"Auto Sync": "Automatisk synkronisering"
"Auto Sync": "Automatisk synkronisering",
"Purged book data: {{title}}": "Bokdata rensad: {{title}}",
"Failed to purge book data: {{title}}": "Det gick inte att rensa bokdata: {{title}}",
"Purge Book Data": "Rensa all bokdata",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Detta raderar permanent boken och all dess data, inklusive läsförlopp, anteckningar och bokmärken. Detta kan inte ångras.",
"Purge Data": "Rensa all data",
"Erase the book and all its data, including reading progress": "Raderar boken och all data, inklusive läsförlopp",
"More Actions": "Fler åtgärder",
"Sign in and make the book available to share it": "Logga in och gör boken tillgänglig för att dela den",
"Download the book to export it": "Ladda ner boken för att exportera den"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "காத்திருப்பதை அழிக்கவும்",
"Reading statistics": "வாசிப்பு புள்ளிவிவரங்கள்",
"Reading time and pages read, synced across your devices and KOReader.": "வாசிப்பு நேரம் மற்றும் படித்த பக்கங்கள், உங்கள் சாதனங்கள் மற்றும் KOReader இடையே ஒத்திசைக்கப்படும்.",
"Auto Sync": "தானியங்கு ஒத்திசைவு"
"Auto Sync": "தானியங்கு ஒத்திசைவு",
"Purged book data: {{title}}": "புத்தகத் தரவு அழிக்கப்பட்டது: {{title}}",
"Failed to purge book data: {{title}}": "புத்தகத் தரவை அழிக்க முடியவில்லை: {{title}}",
"Purge Book Data": "புத்தகத்தின் எல்லா தரவையும் அழி",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "இது புத்தகத்தையும் வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புத்தகக்குறிகள் உள்ளிட்ட அதன் அனைத்து தரவையும் நிரந்தரமாக அழிக்கும். இதை மீட்டெடுக்க முடியாது.",
"Purge Data": "எல்லா தரவையும் அழி",
"Erase the book and all its data, including reading progress": "வாசிப்பு முன்னேற்றம் உட்பட புத்தகத்தையும் அதன் எல்லா தரவையும் அழிக்கும்",
"More Actions": "மேலும் செயல்கள்",
"Sign in and make the book available to share it": "பகிர உள்நுழைந்து புத்தகத்தைக் கிடைக்கச் செய்யவும்",
"Download the book to export it": "ஏற்றுமதி செய்ய புத்தகத்தைப் பதிவிறக்கவும்"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "ล้างที่รอดำเนินการ",
"Reading statistics": "สถิติการอ่าน",
"Reading time and pages read, synced across your devices and KOReader.": "เวลาในการอ่านและจำนวนหน้าที่อ่าน ซิงค์ข้ามอุปกรณ์ของคุณและ KOReader",
"Auto Sync": "ซิงค์อัตโนมัติ"
"Auto Sync": "ซิงค์อัตโนมัติ",
"Purged book data: {{title}}": "ล้างข้อมูลหนังสือแล้ว: {{title}}",
"Failed to purge book data: {{title}}": "ล้างข้อมูลหนังสือไม่สำเร็จ: {{title}}",
"Purge Book Data": "ล้างข้อมูลหนังสือทั้งหมด",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "การดำเนินการนี้จะลบหนังสือและข้อมูลทั้งหมดอย่างถาวร รวมถึงความคืบหน้าการอ่าน บันทึก และที่คั่นหนังสือ ไม่สามารถเลิกทำได้",
"Purge Data": "ล้างข้อมูลทั้งหมด",
"Erase the book and all its data, including reading progress": "ลบหนังสือและข้อมูลทั้งหมด รวมถึงความคืบหน้าการอ่าน",
"More Actions": "การทำงานเพิ่มเติม",
"Sign in and make the book available to share it": "ลงชื่อเข้าใช้และทำให้หนังสือพร้อมใช้งานเพื่อแชร์",
"Download the book to export it": "ดาวน์โหลดหนังสือเพื่อส่งออก"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Bekleyenleri Temizle",
"Reading statistics": "Okuma İstatistikleri",
"Reading time and pages read, synced across your devices and KOReader.": "Okuma süresi ve okunan sayfalar, cihazlarınız ve KOReader arasında senkronize edilir.",
"Auto Sync": "Otomatik Senkronizasyon"
"Auto Sync": "Otomatik Senkronizasyon",
"Purged book data: {{title}}": "Kitap verileri silindi: {{title}}",
"Failed to purge book data: {{title}}": "Kitap verileri silinemedi: {{title}}",
"Purge Book Data": "Tüm kitap verilerini sil",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Bu işlem kitabı ve okuma ilerlemesi, notlar ve yer imleri dahil tüm verilerini kalıcı olarak siler. Bu işlem geri alınamaz.",
"Purge Data": "Tüm verileri sil",
"Erase the book and all its data, including reading progress": "Kitabı ve okuma ilerlemesi dahil tüm verilerini siler",
"More Actions": "Diğer eylemler",
"Sign in and make the book available to share it": "Paylaşmak için oturum açın ve kitabı kullanılabilir hale getirin",
"Download the book to export it": "Dışa aktarmak için kitabı indirin"
}
@@ -1823,5 +1823,14 @@
"Clear Pending": "Очистити очікувані",
"Reading statistics": "Статистика читання",
"Reading time and pages read, synced across your devices and KOReader.": "Час читання та прочитані сторінки, синхронізуються між вашими пристроями та KOReader.",
"Auto Sync": "Автоматична синхронізація"
"Auto Sync": "Автоматична синхронізація",
"Purged book data: {{title}}": "Дані книги очищено: {{title}}",
"Failed to purge book data: {{title}}": "Не вдалося очистити дані книги: {{title}}",
"Purge Book Data": "Очистити дані книги",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Це назавжди видалить книгу та всі її дані, зокрема прогрес читання, нотатки та закладки. Цю дію неможливо скасувати.",
"Purge Data": "Очистити дані",
"Erase the book and all its data, including reading progress": "Видаляє книгу та всі її дані, зокрема прогрес читання",
"More Actions": "Більше дій",
"Sign in and make the book available to share it": "Увійдіть і зробіть книгу доступною, щоб поділитися нею",
"Download the book to export it": "Завантажте книгу, щоб експортувати її"
}
@@ -1757,5 +1757,14 @@
"Clear Pending": "Kutilayotganlarni tozalash",
"Reading statistics": "Oʻqish statistikasi",
"Reading time and pages read, synced across your devices and KOReader.": "Oʻqish vaqti va oʻqilgan sahifalar, qurilmalaringiz va KOReader oʻrtasida sinxronlanadi.",
"Auto Sync": "Avtomatik sinxronlash"
"Auto Sync": "Avtomatik sinxronlash",
"Purged book data: {{title}}": "Kitob maʼlumotlari oʻchirildi: {{title}}",
"Failed to purge book data: {{title}}": "Kitob maʼlumotlarini oʻchirib boʻlmadi: {{title}}",
"Purge Book Data": "Kitobning barcha maʼlumotlarini oʻchirish",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Bu kitobni va uning barcha maʼlumotlarini, jumladan oʻqish jarayoni, eslatmalar va xatchoplarni butunlay oʻchiradi. Buni qaytarib boʻlmaydi.",
"Purge Data": "Barcha maʼlumotlarni oʻchirish",
"Erase the book and all its data, including reading progress": "Kitobni va uning barcha maʼlumotlarini, jumladan oʻqish jarayonini oʻchiradi",
"More Actions": "Qoʻshimcha amallar",
"Sign in and make the book available to share it": "Ulashish uchun tizimga kiring va kitobni mavjud qiling",
"Download the book to export it": "Eksport qilish uchun kitobni yuklab oling"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "Xóa đang chờ",
"Reading statistics": "Thống kê đọc",
"Reading time and pages read, synced across your devices and KOReader.": "Thời gian đọc và số trang đã đọc, được đồng bộ trên các thiết bị của bạn và KOReader.",
"Auto Sync": "Tự động đồng bộ"
"Auto Sync": "Tự động đồng bộ",
"Purged book data: {{title}}": "Đã xóa dữ liệu sách: {{title}}",
"Failed to purge book data: {{title}}": "Không thể xóa dữ liệu sách: {{title}}",
"Purge Book Data": "Xóa toàn bộ dữ liệu sách",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "Thao tác này sẽ xóa vĩnh viễn sách và toàn bộ dữ liệu của nó, bao gồm tiến độ đọc, ghi chú và dấu trang. Không thể hoàn tác.",
"Purge Data": "Xóa toàn bộ dữ liệu",
"Erase the book and all its data, including reading progress": "Xóa sách và toàn bộ dữ liệu, bao gồm tiến độ đọc",
"More Actions": "Thao tác khác",
"Sign in and make the book available to share it": "Đăng nhập và tải sách về để chia sẻ",
"Download the book to export it": "Tải sách về để xuất ra"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "清除等待中",
"Reading statistics": "阅读统计",
"Reading time and pages read, synced across your devices and KOReader.": "阅读时间和已读页数,在您的设备和 KOReader 之间同步。",
"Auto Sync": "自动同步"
"Auto Sync": "自动同步",
"Purged book data: {{title}}": "已清除书籍数据:{{title}}",
"Failed to purge book data: {{title}}": "清除书籍数据失败:{{title}}",
"Purge Book Data": "清除书籍数据",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "这将永久删除该书籍及其所有数据,包括阅读进度、笔记和书签。此操作无法撤销。",
"Purge Data": "清除数据",
"Erase the book and all its data, including reading progress": "删除该书籍及其所有数据,包括阅读进度",
"More Actions": "更多操作",
"Sign in and make the book available to share it": "登录并使书籍可用后即可分享",
"Download the book to export it": "下载书籍后即可导出"
}
@@ -1724,5 +1724,14 @@
"Clear Pending": "清除等待中",
"Reading statistics": "閱讀統計",
"Reading time and pages read, synced across your devices and KOReader.": "閱讀時間與已讀頁數,在您的裝置與 KOReader 之間同步。",
"Auto Sync": "自動同步"
"Auto Sync": "自動同步",
"Purged book data: {{title}}": "已清除書籍資料:{{title}}",
"Failed to purge book data: {{title}}": "清除書籍資料失敗:{{title}}",
"Purge Book Data": "清除書籍資料",
"This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.": "這將永久刪除該書籍及其所有資料,包括閱讀進度、筆記和書籤。此操作無法復原。",
"Purge Data": "清除資料",
"Erase the book and all its data, including reading progress": "刪除該書籍及其所有資料,包括閱讀進度",
"More Actions": "更多操作",
"Sign in and make the book available to share it": "登入並使書籍可用後即可分享",
"Download the book to export it": "下載書籍後即可匯出"
}
@@ -17,6 +17,10 @@ vi.mock('@/context/EnvContext', () => ({
useEnv: () => ({ envConfig: { getAppService: async () => appSvc }, appService: appSvc }),
}));
vi.mock('@/context/AuthContext', () => ({
useAuth: () => ({ user: { id: 'u1' } }),
}));
vi.mock('@/store/themeStore', () => ({
useThemeStore: () => ({ safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 } }),
}));
@@ -29,6 +29,10 @@ vi.mock('@/helpers/settings', () => ({
saveSysSettings: vi.fn(),
}));
vi.mock('@/utils/open', () => ({
openExternalUrl: vi.fn(),
}));
vi.mock('@/components/BookCover', () => ({
__esModule: true,
default: () => null,
@@ -103,6 +107,85 @@ describe('BookDetailView delete dropdown layout', () => {
});
});
describe('BookDetailView More menu (Goodreads + Share)', () => {
const openMore = (container: HTMLElement) => {
const toggle = container.querySelector('button[aria-label="More Actions"]');
expect(toggle).toBeTruthy();
fireEvent.click(toggle!);
};
it('folds Goodreads and Share into the hamburger menu', () => {
const { container, getByText } = renderView({ onShare: vi.fn(), shareEnabled: true });
// Goodreads is no longer a standalone icon button outside the menu.
expect(container.querySelector('button[aria-label="More Actions"]')).toBeTruthy();
openMore(container);
expect(getByText('Search on Goodreads')).toBeTruthy();
expect(getByText('Share Book')).toBeTruthy();
});
it('enables Share and calls onShare when the book is shareable', () => {
const onShare = vi.fn();
const { container, getByText } = renderView({ onShare, shareEnabled: true });
openMore(container);
const shareButton = getByText('Share Book').closest('button');
expect(shareButton).toBeTruthy();
expect(shareButton!.disabled).toBe(false);
fireEvent.click(shareButton!);
expect(onShare).toHaveBeenCalledTimes(1);
});
it('disables Share when not shareable (logged out or no local file)', () => {
const onShare = vi.fn();
const { container, getByText } = renderView({ onShare, shareEnabled: false });
openMore(container);
const shareButton = getByText('Share Book').closest('button');
expect(shareButton!.disabled).toBe(true);
fireEvent.click(shareButton!);
expect(onShare).not.toHaveBeenCalled();
});
it('keeps Export in the More menu and calls onExport when the file exists', () => {
const onExport = vi.fn();
const { container, getByText } = renderView({ onExport, fileSize: 1024 });
openMore(container);
const exportButton = getByText('Export Book').closest('button');
expect(exportButton).toBeTruthy();
expect(exportButton!.disabled).toBe(false);
fireEvent.click(exportButton!);
expect(onExport).toHaveBeenCalledTimes(1);
});
it('disables Export when the book has no local file', () => {
const onExport = vi.fn();
const { container, getByText } = renderView({ onExport, fileSize: null });
openMore(container);
const exportButton = getByText('Export Book').closest('button');
expect(exportButton!.disabled).toBe(true);
fireEvent.click(exportButton!);
expect(onExport).not.toHaveBeenCalled();
});
});
describe('BookDetailView Purge Data action', () => {
it('offers Purge Data in the delete dropdown and calls onPurge', () => {
const onPurge = vi.fn();
const { container, getByText } = renderView({ onPurge });
const toggle = container.querySelector('button[aria-label="Delete Book Options"]');
fireEvent.click(toggle!);
const purgeButton = getByText('Purge Data').closest('button');
expect(purgeButton).toBeTruthy();
fireEvent.click(purgeButton!);
expect(onPurge).toHaveBeenCalledTimes(1);
});
it('omits Purge Data when onPurge is not provided', () => {
const { container, queryByText } = renderView();
const toggle = container.querySelector('button[aria-label="Delete Book Options"]');
fireEvent.click(toggle!);
expect(queryByText('Purge Data')).toBeNull();
});
});
describe('BookDetailView file path row', () => {
// book.filePath is only set for in-place imports (and OS-handed paths like
// Android "Open with Readest"). Hash-copy imports leave it undefined, so
@@ -176,6 +176,73 @@ describe('cloudService', () => {
});
});
// Purge is the "Cloud & Device" delete PLUS a full wipe of the
// app-generated Books/<hash>/ directory (config.json reading progress/notes,
// nav.json, cover.png) that the other deletes leave behind (issue #4615).
// The local-file side lives here; the tombstone + queued cloud deletion are
// owned by the page's handleBookDelete, exactly like the 'both'/'local'
// split, so this branch must NOT touch deletedAt or the cloud.
describe('purge delete action', () => {
test('removes the entire Books/<hash>/ directory', async () => {
const book = createMockBook();
await deleteBook(mockFs, book, 'purge');
expect(mockFs.removeDir).toHaveBeenCalledWith(book.hash, 'Books', true);
});
test('does not remove the managed book file individually (the dir wipe covers it)', async () => {
const book = createMockBook();
await deleteBook(mockFs, book, 'purge');
// The whole directory is removed in one shot; no per-file removeFile.
expect(mockFs.removeFile).not.toHaveBeenCalled();
});
test('clears downloadedAt but leaves deletedAt for the caller (mirrors local)', async () => {
const book = createMockBook({ downloadedAt: 12345, deletedAt: null });
await deleteBook(mockFs, book, 'purge');
expect(book.downloadedAt).toBeNull();
expect(book.deletedAt).toBeNull();
});
test('does not delete cloud files (the page queues the cloud deletion)', async () => {
const { deleteFile: deleteCloudFile } = await import('@/libs/storage');
const book = createMockBook({ uploadedAt: 1000 });
await deleteBook(mockFs, book, 'purge');
expect(deleteCloudFile).not.toHaveBeenCalled();
// uploadedAt is left intact for the queued cloud-delete transfer to clear.
expect(book.uploadedAt).toBe(1000);
});
test('removes the in-place source file and the sidecar dir for in-place books', async () => {
const book = createMockBook({ filePath: '/Users/me/Library/sample.epub' });
vi.mocked(mockFs.exists).mockImplementation(async (path, base) => {
if (base === 'None' && path === book.filePath) return true;
if (base === 'Books' && path === book.hash) return true;
return false;
});
await deleteBook(mockFs, book, 'purge');
// External source file (outside Books/<hash>/) is removed...
expect(mockFs.removeFile).toHaveBeenCalledWith('/Users/me/Library/sample.epub', 'None');
// ...and the metadata sidecar directory is wiped too.
expect(mockFs.removeDir).toHaveBeenCalledWith(book.hash, 'Books', true);
});
test('does not throw when the directory does not exist', async () => {
vi.mocked(mockFs.exists).mockResolvedValue(false);
const book = createMockBook({ downloadedAt: 12345 });
await deleteBook(mockFs, book, 'purge');
expect(mockFs.removeDir).not.toHaveBeenCalled();
expect(book.downloadedAt).toBeNull();
});
});
describe('cloud delete action', () => {
test('does not delete local files', async () => {
const book = createMockBook({ uploadedAt: 1000 });
+10 -5
View File
@@ -877,18 +877,22 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
both: _('Book deleted: {{title}}', { title: book.title }),
cloud: _('Deleted cloud backup of the book: {{title}}', { title: book.title }),
local: _('Deleted local copy of the book: {{title}}', { title: book.title }),
purge: _('Purged book data: {{title}}', { title: book.title }),
};
const deletionFailMessages = {
both: _('Failed to delete book: {{title}}', { title: book.title }),
cloud: _('Failed to delete cloud backup of the book: {{title}}', { title: book.title }),
local: _('Failed to delete local copy of the book: {{title}}', { title: book.title }),
purge: _('Failed to purge book data: {{title}}', { title: book.title }),
};
try {
// Handle local deletion immediately
if (deleteAction === 'local' || deleteAction === 'both') {
await appService?.deleteBook(book, 'local');
if (deleteAction === 'both') {
// Handle local deletion immediately. Purge mirrors 'both' (tombstone +
// queued cloud delete) but hands 'purge' to deleteBook, which also wipes
// the entire Books/<hash>/ folder (config/nav/cover) — issue #4615.
if (deleteAction === 'local' || deleteAction === 'both' || deleteAction === 'purge') {
await appService?.deleteBook(book, deleteAction === 'purge' ? 'purge' : 'local');
if (deleteAction === 'both' || deleteAction === 'purge') {
book.deletedAt = Date.now();
book.downloadedAt = null;
book.coverDownloadedAt = null;
@@ -899,7 +903,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
// Queue cloud deletion
if (deleteAction === 'cloud' || deleteAction === 'both') {
if (deleteAction === 'cloud' || deleteAction === 'both' || deleteAction === 'purge') {
const transferId = transferManager.queueDelete(book, 1, true);
if (!transferId) {
throw new Error('Failed to queue cloud deletion');
@@ -1468,6 +1472,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
handleBookDelete={handleBookDelete('both')}
handleBookDeleteCloudBackup={handleBookDelete('cloud')}
handleBookDeleteLocalCopy={handleBookDelete('local')}
handleBookPurge={handleBookDelete('purge')}
handleBookMetadataUpdate={handleUpdateMetadata}
/>
)}
@@ -5,6 +5,7 @@ import { Book } from '@/types/book';
import { getBookWithUpdatedMetadata } from '@/utils/book';
import { BookMetadata } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useMetadataEdit } from './useMetadataEdit';
@@ -27,6 +28,7 @@ interface BookDetailModalProps {
handleBookDelete?: (book: Book) => void;
handleBookDeleteCloudBackup?: (book: Book) => void;
handleBookDeleteLocalCopy?: (book: Book) => void;
handleBookPurge?: (book: Book) => void;
handleBookMetadataUpdate?: (book: Book, updatedMetadata: BookMetadata) => void;
}
@@ -45,10 +47,12 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
handleBookDelete,
handleBookDeleteCloudBackup,
handleBookDeleteLocalCopy,
handleBookPurge,
handleBookMetadataUpdate,
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { user } = useAuth();
const { safeAreaInsets } = useThemeStore();
const [activeDeleteAction, setActiveDeleteAction] = useState<DeleteAction | null>(null);
const [isLoading, setIsLoading] = useState(false);
@@ -95,6 +99,13 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
message: _('Are you sure to delete the local copy of the selected book?'),
handler: handleBookDeleteLocalCopy,
},
purge: {
title: _('Purge Book Data'),
message: _(
'This permanently erases the book and all its data, including reading progress, notes, and bookmarks. This cannot be undone.',
),
handler: handleBookPurge,
},
};
useEffect(() => {
@@ -168,19 +179,14 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
const handleDelete = () => handleDeleteAction('both');
const handleDeleteCloudBackup = () => handleDeleteAction('cloud');
const handleDeleteLocalCopy = () => handleDeleteAction('local');
const handlePurge = () => handleDeleteAction('purge');
const handleRedownload = async () => {
const handleShare = () => {
// Close this modal first, then hand off to the share dialog hosted by
// Bookshelf (it owns the login gate + ShareBookDialog). Mirrors how the
// bookshelf context menu dispatches the same event.
handleClose();
if (handleBookDownload) {
handleBookDownload(book, { redownload: true, queued: false });
}
};
const handleReupload = async () => {
handleClose();
if (handleBookUpload) {
handleBookUpload(book);
}
eventDispatcher.dispatch('show-share-dialog', { book });
};
const handleBookExport = async () => {
@@ -197,6 +203,25 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
}, 0);
};
const handleRedownload = async () => {
handleClose();
if (handleBookDownload) {
handleBookDownload(book, { redownload: true, queued: false });
}
};
const handleReupload = async () => {
handleClose();
if (handleBookUpload) {
handleBookUpload(book);
}
};
// Sharing uploads the book to the Readest backend and mints a public link, so
// it needs a signed-in user and a resolvable on-disk file. `fileSize` is only
// non-null when getBookFileSize could actually open the local file.
const shareEnabled = !!user && fileSize !== null;
const currentDeleteConfig = activeDeleteAction ? deleteConfigs[activeDeleteAction] : null;
return (
@@ -235,14 +260,17 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
book={displayBook}
metadata={bookMeta}
fileSize={fileSize}
shareEnabled={shareEnabled}
onEdit={handleBookMetadataUpdate ? handleEditMetadata : undefined}
onDelete={handleBookDelete ? handleDelete : undefined}
onDeleteCloudBackup={
handleBookDeleteCloudBackup ? handleDeleteCloudBackup : undefined
}
onDeleteLocalCopy={handleBookDeleteLocalCopy ? handleDeleteLocalCopy : undefined}
onPurge={handleBookPurge ? handlePurge : undefined}
onDownload={handleBookDownload ? handleRedownload : undefined}
onUpload={handleBookUpload ? handleReupload : undefined}
onShare={handleShare}
onExport={handleBookExport}
/>
)}
@@ -5,11 +5,10 @@ import {
MdOutlineCloudUpload,
MdOutlineDelete,
MdOutlineEdit,
MdSaveAlt,
MdMenu,
MdExpandMore,
MdExpandLess,
} from 'react-icons/md';
import { FaGoodreads } from 'react-icons/fa';
import { Book } from '@/types/book';
import { BookMetadata } from '@/libs/document';
@@ -35,12 +34,15 @@ interface BookDetailViewProps {
book: Book;
metadata: BookMetadata | null;
fileSize: number | null;
shareEnabled?: boolean;
onEdit?: () => void;
onDelete?: () => void;
onDeleteCloudBackup?: () => void;
onDeleteLocalCopy?: () => void;
onPurge?: () => void;
onDownload?: () => void;
onUpload?: () => void;
onShare?: () => void;
onExport?: () => void;
}
@@ -48,18 +50,25 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
book,
metadata,
fileSize,
shareEnabled,
onEdit,
onDelete,
onDeleteCloudBackup,
onDeleteLocalCopy,
onPurge,
onDownload,
onUpload,
onShare,
onExport,
}) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
// Export and Share both read the book file off disk; `fileSize` is only
// non-null when getBookFileSize could actually open the local copy.
const hasLocalFile = fileSize !== null;
const toggleSeriesCollapse = () => {
saveSysSettings(envConfig, 'metadataSeriesCollapsed', !settings.metadataSeriesCollapsed);
};
@@ -101,12 +110,16 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
<MdOutlineEdit className='hover:fill-blue-500' />
</button>
)}
<button
onClick={() => openExternalUrl(getGoodreadsSearchUrl(getBookGoodreadsQuery(book)))}
title={_('Search on Goodreads')}
>
<FaGoodreads className='fill-base-content' />
</button>
{book.uploadedAt && onDownload && (
<button onClick={onDownload} title={_('Download from Cloud')}>
<MdOutlineCloudDownload className='fill-base-content' />
</button>
)}
{book.downloadedAt && onUpload && (
<button onClick={onUpload} title={_('Upload to Cloud')}>
<MdOutlineCloudUpload className='fill-base-content' />
</button>
)}
{onDelete && (
<Dropdown
label={_('Delete Book Options')}
@@ -140,24 +153,65 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
onClick={onDeleteLocalCopy}
disabled={!book.downloadedAt}
/>
{onPurge && (
<MenuItem
noIcon
transient
label={_('Purge Data')}
labelClass='text-red-500'
tooltip={_('Erase the book and all its data, including reading progress')}
onClick={onPurge}
/>
)}
</div>
</Dropdown>
)}
{book.uploadedAt && onDownload && (
<button onClick={onDownload} title={_('Download from Cloud')}>
<MdOutlineCloudDownload className='fill-base-content' />
</button>
)}
{book.downloadedAt && onUpload && (
<button onClick={onUpload} title={_('Upload to Cloud')}>
<MdOutlineCloudUpload className='fill-base-content' />
</button>
)}
{book.downloadedAt && onExport && (
<button onClick={onExport} title={_('Export Book')}>
<MdSaveAlt className='fill-base-content' />
</button>
)}
<Dropdown
label={_('More Actions')}
className='dropdown-bottom dropdown-center flex justify-center'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdMenu className='fill-base-content' />}
>
<div
className={clsx(
'more-menu dropdown-content no-triangle !relative',
'border-base-300 !bg-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
)}
>
<MenuItem
noIcon
transient
label={_('Search on Goodreads')}
onClick={() =>
openExternalUrl(getGoodreadsSearchUrl(getBookGoodreadsQuery(book)))
}
/>
{onShare && (
<MenuItem
noIcon
transient
label={_('Share Book')}
disabled={!shareEnabled}
tooltip={
shareEnabled
? undefined
: _('Sign in and make the book available to share it')
}
onClick={onShare}
/>
)}
{onExport && (
<MenuItem
noIcon
transient
label={_('Export Book')}
disabled={!hasLocalFile}
tooltip={hasLocalFile ? undefined : _('Download the book to export it')}
onClick={onExport}
/>
)}
</div>
</Dropdown>
</div>
</div>
</div>
+19 -3
View File
@@ -24,7 +24,7 @@ export async function deleteBook(
book: Book,
deleteAction: DeleteAction,
): Promise<void> {
if (deleteAction === 'local' || deleteAction === 'both') {
if (deleteAction === 'local' || deleteAction === 'both' || deleteAction === 'purge') {
const source = await resolveBookContentSource(fs, book);
if (source.kind === 'external') {
try {
@@ -36,16 +36,32 @@ export async function deleteBook(
// the metadata-side bookkeeping that follows.
console.log('Failed to remove in-place source file:', error);
}
} else if (source.kind === 'managed') {
} else if (source.kind === 'managed' && deleteAction !== 'purge') {
// Purge wipes the whole directory below, so skip the per-file removal.
if (await fs.exists(source.path, source.base)) {
await fs.removeFile(source.path, source.base);
}
}
// Purge erases the entire app-generated Books/<hash>/ directory — the
// managed book file, cover.png, and (the reason for issue #4615)
// config.json (reading progress, notes, bookmarks) + nav.json that the
// other delete actions leave behind. For in-place books the external
// source file was already removed above; this clears the sidecar dir.
if (deleteAction === 'purge') {
const dir = getDir(book);
if (await fs.exists(dir, 'Books')) {
await fs.removeDir(dir, 'Books', true);
}
}
if (deleteAction === 'both' && (await fs.exists(getCoverFilename(book), 'Books'))) {
await fs.removeFile(getCoverFilename(book), 'Books');
}
if (deleteAction === 'local') {
if (deleteAction === 'local' || deleteAction === 'purge') {
// Mirror 'local': mark not-downloaded but leave the tombstone (deletedAt)
// to the caller. The page's handleBookDelete sets deletedAt and queues the
// cloud deletion for purge, exactly as it does for the 'both' action.
book.downloadedAt = null;
} else {
book.deletedAt = Date.now();
+1 -1
View File
@@ -15,7 +15,7 @@ export type AppPlatform = 'web' | 'tauri' | 'node';
export type OsPlatform = 'android' | 'ios' | 'macos' | 'windows' | 'linux' | 'unknown';
// biome-ignore format: keep the union members compact on a single line
export type BaseDir = | 'Books' | 'Settings' | 'Data' | 'Fonts' | 'Images' | 'Dictionaries' | 'Log' | 'Cache' | 'Temp' | 'None';
export type DeleteAction = 'cloud' | 'local' | 'both';
export type DeleteAction = 'cloud' | 'local' | 'both' | 'purge';
export type SelectDirectoryMode = 'read' | 'write';
export type DistChannel = 'readest' | 'playstore' | 'appstore' | 'unknown';