feat(library): add Manage Cache to advanced settings (#4359)

Add a "Manage Cache" item to the library Advanced Settings menu (native
mobile apps only) that opens a modern dialog showing the combined size and
file count of the app's reclaimable storage, with a confirm-gated clear that
reports per-file progress.

- iOS clears Cache + Temp + Documents/Inbox; Android clears Cache + Temp.
- Multi-source helper (getCacheEntries/getCacheStats/clearCacheEntries) with
  unit tests; per-file failures are counted, never abort the run.
- Dialog uses the centered-hero + btn-contrast design language, theme-neutral
  progress, and is e-ink correct.
- i18n: new strings translated across all locales (+ en plural forms).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-05-30 01:57:07 +08:00
committed by GitHub
parent 789d031222
commit bed31e8181
39 changed files with 944 additions and 33 deletions
@@ -1807,5 +1807,22 @@
"No bookmark yet": "لا توجد إشارات مرجعية بعد",
"Importing...": "جاري الاستيراد...",
"Swipe for Brightness": "اسحب لضبط السطوع",
"Slide along the left edge": "اسحب على طول الحافة اليسرى"
"Slide along the left edge": "اسحب على طول الحافة اليسرى",
"Failed to read cache": "فشل قراءة ذاكرة التخزين المؤقت",
"Failed to clear cache": "فشل مسح ذاكرة التخزين المؤقت",
"Calculating cache size...": "جارٍ حساب حجم ذاكرة التخزين المؤقت…",
"Cache cleared": "تم مسح ذاكرة التخزين المؤقت",
"{{count}} files_zero": "{{count}} ملف",
"{{count}} files_one": "ملف واحد",
"{{count}} files_two": "ملفان",
"{{count}} files_few": "{{count}} ملفات",
"{{count}} files_many": "{{count}} ملفًا",
"{{count}} files_other": "{{count}} ملف",
"Manage Cache": "إدارة ذاكرة التخزين المؤقت",
"Deleting: {{file}}": "جارٍ الحذف: {{file}}",
"Clearing cache...": "جارٍ مسح ذاكرة التخزين المؤقت…",
"This will delete all cached files. This cannot be undone.": "سيؤدي هذا إلى حذف جميع الملفات المخزنة مؤقتًا. لا يمكن التراجع عن هذا الإجراء.",
"Confirm Clear": "تأكيد المسح",
"Clearing...": "جارٍ المسح…",
"Clear Cache": "مسح ذاكرة التخزين المؤقت"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "এখনও কোনো বুকমার্ক নেই",
"Importing...": "আমদানি হচ্ছে...",
"Swipe for Brightness": "উজ্জ্বলতার জন্য সোয়াইপ করুন",
"Slide along the left edge": "বাম প্রান্ত বরাবর স্লাইড করুন"
"Slide along the left edge": "বাম প্রান্ত বরাবর স্লাইড করুন",
"Failed to read cache": "ক্যাশ পড়া ব্যর্থ হয়েছে",
"Failed to clear cache": "ক্যাশ সাফ করা ব্যর্থ হয়েছে",
"Calculating cache size...": "ক্যাশের আকার গণনা করা হচ্ছে…",
"Cache cleared": "ক্যাশ সাফ করা হয়েছে",
"{{count}} files_one": "{{count}}টি ফাইল",
"{{count}} files_other": "{{count}}টি ফাইল",
"Manage Cache": "ক্যাশ পরিচালনা করুন",
"Deleting: {{file}}": "মুছে ফেলা হচ্ছে: {{file}}",
"Clearing cache...": "ক্যাশ সাফ করা হচ্ছে…",
"This will delete all cached files. This cannot be undone.": "এটি সমস্ত ক্যাশ করা ফাইল মুছে ফেলবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
"Confirm Clear": "সাফ করা নিশ্চিত করুন",
"Clearing...": "সাফ করা হচ্ছে…",
"Clear Cache": "ক্যাশ সাফ করুন"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "ད་དུང་དཔེ་རྟགས་མེད།",
"Importing...": "ནང་འདྲེན་བྱེད་བཞིན་པ...",
"Swipe for Brightness": "འོད་གདངས་ཆེད་འཐེན།",
"Slide along the left edge": "གཡོན་ངོས་ཟུར་ནས་འཐེན།"
"Slide along the left edge": "གཡོན་ངོས་ཟུར་ནས་འཐེན།",
"Failed to read cache": "གསོག་ཉར་ཀློག་མ་ཐུབ།",
"Failed to clear cache": "གསོག་ཉར་བསལ་མ་ཐུབ།",
"Calculating cache size...": "གསོག་ཉར་གྱི་ཆེ་ཆུང་རྩིས་བཞིན་པ།...",
"Cache cleared": "གསོག་ཉར་བསལ་ཟིན།",
"{{count}} files_other": "ཡིག་ཆ་ {{count}}",
"Manage Cache": "གསོག་ཉར་དོ་དམ།",
"Deleting: {{file}}": "བསུབ་བཞིན་པ། {{file}}",
"Clearing cache...": "གསོག་ཉར་བསལ་བཞིན་པ།...",
"This will delete all cached files. This cannot be undone.": "འདིས་གསོག་ཉར་གྱི་ཡིག་ཆ་ཚང་མ་བསུབ་ངེས་ཡིན། འདི་སྔར་བཞིན་བཟོ་མི་ཐུབ།",
"Confirm Clear": "བསལ་བ་གཏན་འཁེལ།",
"Clearing...": "བསལ་བཞིན་པ།...",
"Clear Cache": "གསོག་ཉར་བསལ་བ།"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Noch kein Lesezeichen",
"Importing...": "Wird importiert...",
"Swipe for Brightness": "Wischen für Helligkeit",
"Slide along the left edge": "Am linken Rand entlang wischen"
"Slide along the left edge": "Am linken Rand entlang wischen",
"Failed to read cache": "Cache konnte nicht gelesen werden",
"Failed to clear cache": "Cache konnte nicht geleert werden",
"Calculating cache size...": "Cache-Größe wird berechnet …",
"Cache cleared": "Cache geleert",
"{{count}} files_one": "{{count}} Datei",
"{{count}} files_other": "{{count}} Dateien",
"Manage Cache": "Cache verwalten",
"Deleting: {{file}}": "Wird gelöscht: {{file}}",
"Clearing cache...": "Cache wird geleert …",
"This will delete all cached files. This cannot be undone.": "Dadurch werden alle zwischengespeicherten Dateien gelöscht. Dies kann nicht rückgängig gemacht werden.",
"Confirm Clear": "Leeren bestätigen",
"Clearing...": "Wird geleert …",
"Clear Cache": "Cache leeren"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Δεν υπάρχει ακόμη σελιδοδείκτης",
"Importing...": "Εισαγωγή...",
"Swipe for Brightness": "Σάρωση για φωτεινότητα",
"Slide along the left edge": "Σύρετε κατά μήκος της αριστερής άκρης"
"Slide along the left edge": "Σύρετε κατά μήκος της αριστερής άκρης",
"Failed to read cache": "Αποτυχία ανάγνωσης της προσωρινής μνήμης",
"Failed to clear cache": "Αποτυχία εκκαθάρισης της προσωρινής μνήμης",
"Calculating cache size...": "Υπολογισμός μεγέθους προσωρινής μνήμης…",
"Cache cleared": "Η προσωρινή μνήμη εκκαθαρίστηκε",
"{{count}} files_one": "{{count}} αρχείο",
"{{count}} files_other": "{{count}} αρχεία",
"Manage Cache": "Διαχείριση προσωρινής μνήμης",
"Deleting: {{file}}": "Διαγραφή: {{file}}",
"Clearing cache...": "Εκκαθάριση προσωρινής μνήμης…",
"This will delete all cached files. This cannot be undone.": "Αυτό θα διαγράψει όλα τα προσωρινά αποθηκευμένα αρχεία. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"Confirm Clear": "Επιβεβαίωση εκκαθάρισης",
"Clearing...": "Εκκαθάριση…",
"Clear Cache": "Εκκαθάριση προσωρινής μνήμης"
}
@@ -14,6 +14,8 @@
"{{count}} pages left in book_other": "<0>{{count}}</0><1> pages left in book</1>",
"Deleted {{count}} file(s)_one": "Deleted {{count}} file",
"Deleted {{count}} file(s)_other": "Deleted {{count}} files",
"{{count}} files_one": "{{count}} file",
"{{count}} files_other": "{{count}} files",
"Failed to delete {{count}} file(s)_one": "Failed to delete {{count}} file",
"Failed to delete {{count}} file(s)_other": "Failed to delete {{count}} files",
"Failed to import {{count}} books_one": "Failed to import {{count}} book",
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Aún no hay marcadores",
"Importing...": "Importando...",
"Swipe for Brightness": "Deslizar para el brillo",
"Slide along the left edge": "Desliza por el borde izquierdo"
"Slide along the left edge": "Desliza por el borde izquierdo",
"Failed to read cache": "No se pudo leer la caché",
"Failed to clear cache": "No se pudo borrar la caché",
"Calculating cache size...": "Calculando el tamaño de la caché…",
"Cache cleared": "Caché borrada",
"{{count}} files_one": "{{count}} archivo",
"{{count}} files_many": "{{count}} archivos",
"{{count}} files_other": "{{count}} archivos",
"Manage Cache": "Administrar caché",
"Deleting: {{file}}": "Eliminando: {{file}}",
"Clearing cache...": "Borrando caché…",
"This will delete all cached files. This cannot be undone.": "Esto eliminará todos los archivos en caché. Esta acción no se puede deshacer.",
"Confirm Clear": "Confirmar borrado",
"Clearing...": "Borrando…",
"Clear Cache": "Borrar caché"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "هنوز نشانکی وجود ندارد",
"Importing...": "در حال وارد کردن...",
"Swipe for Brightness": "کشیدن برای روشنایی",
"Slide along the left edge": "در امتداد لبه چپ بکشید"
"Slide along the left edge": "در امتداد لبه چپ بکشید",
"Failed to read cache": "خواندن حافظهٔ پنهان ناموفق بود",
"Failed to clear cache": "پاک کردن حافظهٔ پنهان ناموفق بود",
"Calculating cache size...": "در حال محاسبهٔ اندازهٔ حافظهٔ پنهان…",
"Cache cleared": "حافظهٔ پنهان پاک شد",
"{{count}} files_one": "{{count}} فایل",
"{{count}} files_other": "{{count}} فایل",
"Manage Cache": "مدیریت حافظهٔ پنهان",
"Deleting: {{file}}": "در حال حذف: {{file}}",
"Clearing cache...": "در حال پاک کردن حافظهٔ پنهان…",
"This will delete all cached files. This cannot be undone.": "این کار همهٔ فایل‌های ذخیره‌شده در حافظهٔ پنهان را حذف می‌کند. این عمل قابل بازگشت نیست.",
"Confirm Clear": "تأیید پاک کردن",
"Clearing...": "در حال پاک کردن…",
"Clear Cache": "پاک کردن حافظهٔ پنهان"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Aucun signet pour le moment",
"Importing...": "Importation...",
"Swipe for Brightness": "Balayer pour la luminosité",
"Slide along the left edge": "Glissez le long du bord gauche"
"Slide along the left edge": "Glissez le long du bord gauche",
"Failed to read cache": "Échec de la lecture du cache",
"Failed to clear cache": "Échec du vidage du cache",
"Calculating cache size...": "Calcul de la taille du cache…",
"Cache cleared": "Cache vidé",
"{{count}} files_one": "{{count}} fichier",
"{{count}} files_many": "{{count}} fichiers",
"{{count}} files_other": "{{count}} fichiers",
"Manage Cache": "Gérer le cache",
"Deleting: {{file}}": "Suppression : {{file}}",
"Clearing cache...": "Vidage du cache…",
"This will delete all cached files. This cannot be undone.": "Cette action supprimera tous les fichiers mis en cache. Elle est irréversible.",
"Confirm Clear": "Confirmer le vidage",
"Clearing...": "Vidage…",
"Clear Cache": "Vider le cache"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "אין סימניות עדיין",
"Importing...": "מייבא...",
"Swipe for Brightness": "החלקה לכוונון בהירות",
"Slide along the left edge": "החליקו לאורך הקצה השמאלי"
"Slide along the left edge": "החליקו לאורך הקצה השמאלי",
"Failed to read cache": "קריאת המטמון נכשלה",
"Failed to clear cache": "ניקוי המטמון נכשל",
"Calculating cache size...": "מחשב את גודל המטמון…",
"Cache cleared": "המטמון נוקה",
"{{count}} files_one": "קובץ אחד",
"{{count}} files_two": "{{count}} קבצים",
"{{count}} files_other": "{{count}} קבצים",
"Manage Cache": "ניהול מטמון",
"Deleting: {{file}}": "מוחק: {{file}}",
"Clearing cache...": "מנקה מטמון…",
"This will delete all cached files. This cannot be undone.": "פעולה זו תמחק את כל הקבצים השמורים במטמון. לא ניתן לבטל פעולה זו.",
"Confirm Clear": "אישור ניקוי",
"Clearing...": "מנקה…",
"Clear Cache": "נקה מטמון"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "अभी तक कोई बुकमार्क नहीं",
"Importing...": "आयात हो रहा है...",
"Swipe for Brightness": "चमक के लिए स्वाइप करें",
"Slide along the left edge": "बाएँ किनारे पर स्लाइड करें"
"Slide along the left edge": "बाएँ किनारे पर स्लाइड करें",
"Failed to read cache": "कैश पढ़ने में विफल",
"Failed to clear cache": "कैश साफ़ करने में विफल",
"Calculating cache size...": "कैश का आकार परिकलित किया जा रहा है…",
"Cache cleared": "कैश साफ़ कर दिया गया",
"{{count}} files_one": "{{count}} फ़ाइल",
"{{count}} files_other": "{{count}} फ़ाइलें",
"Manage Cache": "कैश प्रबंधित करें",
"Deleting: {{file}}": "हटाया जा रहा है: {{file}}",
"Clearing cache...": "कैश साफ़ किया जा रहा है…",
"This will delete all cached files. This cannot be undone.": "इससे सभी कैश की गई फ़ाइलें हट जाएँगी. इसे पूर्ववत नहीं किया जा सकता.",
"Confirm Clear": "साफ़ करने की पुष्टि करें",
"Clearing...": "साफ़ किया जा रहा है…",
"Clear Cache": "कैश साफ़ करें"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Még nincs könyvjelző",
"Importing...": "Importálás...",
"Swipe for Brightness": "Húzás a fényerőhöz",
"Slide along the left edge": "Húzza a bal szél mentén"
"Slide along the left edge": "Húzza a bal szél mentén",
"Failed to read cache": "Nem sikerült olvasni a gyorsítótárat",
"Failed to clear cache": "Nem sikerült törölni a gyorsítótárat",
"Calculating cache size...": "Gyorsítótár méretének kiszámítása…",
"Cache cleared": "Gyorsítótár törölve",
"{{count}} files_one": "{{count}} fájl",
"{{count}} files_other": "{{count}} fájl",
"Manage Cache": "Gyorsítótár kezelése",
"Deleting: {{file}}": "Törlés: {{file}}",
"Clearing cache...": "Gyorsítótár törlése…",
"This will delete all cached files. This cannot be undone.": "Ez törli az összes gyorsítótárazott fájlt. A művelet nem vonható vissza.",
"Confirm Clear": "Törlés megerősítése",
"Clearing...": "Törlés…",
"Clear Cache": "Gyorsítótár törlése"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "Belum ada penanda",
"Importing...": "Mengimpor...",
"Swipe for Brightness": "Geser untuk kecerahan",
"Slide along the left edge": "Geser di sepanjang tepi kiri"
"Slide along the left edge": "Geser di sepanjang tepi kiri",
"Failed to read cache": "Gagal membaca cache",
"Failed to clear cache": "Gagal membersihkan cache",
"Calculating cache size...": "Menghitung ukuran cache…",
"Cache cleared": "Cache dibersihkan",
"{{count}} files_other": "{{count}} file",
"Manage Cache": "Kelola Cache",
"Deleting: {{file}}": "Menghapus: {{file}}",
"Clearing cache...": "Membersihkan cache…",
"This will delete all cached files. This cannot be undone.": "Tindakan ini akan menghapus semua file cache. Tindakan ini tidak dapat dibatalkan.",
"Confirm Clear": "Konfirmasi Pembersihan",
"Clearing...": "Membersihkan…",
"Clear Cache": "Bersihkan Cache"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Nessun segnalibro ancora",
"Importing...": "Importazione...",
"Swipe for Brightness": "Scorri per la luminosità",
"Slide along the left edge": "Scorri lungo il bordo sinistro"
"Slide along the left edge": "Scorri lungo il bordo sinistro",
"Failed to read cache": "Impossibile leggere la cache",
"Failed to clear cache": "Impossibile svuotare la cache",
"Calculating cache size...": "Calcolo della dimensione della cache…",
"Cache cleared": "Cache svuotata",
"{{count}} files_one": "{{count}} file",
"{{count}} files_many": "{{count}} file",
"{{count}} files_other": "{{count}} file",
"Manage Cache": "Gestisci cache",
"Deleting: {{file}}": "Eliminazione: {{file}}",
"Clearing cache...": "Svuotamento della cache…",
"This will delete all cached files. This cannot be undone.": "Questa operazione eliminerà tutti i file memorizzati nella cache. Lazione non può essere annullata.",
"Confirm Clear": "Conferma svuotamento",
"Clearing...": "Svuotamento…",
"Clear Cache": "Svuota cache"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "ブックマークはまだありません",
"Importing...": "インポートしています...",
"Swipe for Brightness": "スワイプで明るさ調整",
"Slide along the left edge": "左端をスライド"
"Slide along the left edge": "左端をスライド",
"Failed to read cache": "キャッシュの読み込みに失敗しました",
"Failed to clear cache": "キャッシュの削除に失敗しました",
"Calculating cache size...": "キャッシュサイズを計算中…",
"Cache cleared": "キャッシュを削除しました",
"{{count}} files_other": "{{count}} 件のファイル",
"Manage Cache": "キャッシュの管理",
"Deleting: {{file}}": "削除中: {{file}}",
"Clearing cache...": "キャッシュを削除中…",
"This will delete all cached files. This cannot be undone.": "すべてのキャッシュファイルを削除します。この操作は取り消せません。",
"Confirm Clear": "削除を確認",
"Clearing...": "削除中…",
"Clear Cache": "キャッシュを削除"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "아직 북마크가 없습니다",
"Importing...": "가져오는 중...",
"Swipe for Brightness": "밝기 조절 스와이프",
"Slide along the left edge": "왼쪽 가장자리를 따라 슬라이드"
"Slide along the left edge": "왼쪽 가장자리를 따라 슬라이드",
"Failed to read cache": "캐시를 읽지 못했습니다",
"Failed to clear cache": "캐시를 지우지 못했습니다",
"Calculating cache size...": "캐시 크기 계산 중…",
"Cache cleared": "캐시가 지워졌습니다",
"{{count}} files_other": "파일 {{count}}개",
"Manage Cache": "캐시 관리",
"Deleting: {{file}}": "삭제 중: {{file}}",
"Clearing cache...": "캐시 지우는 중…",
"This will delete all cached files. This cannot be undone.": "캐시된 모든 파일을 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
"Confirm Clear": "지우기 확인",
"Clearing...": "지우는 중…",
"Clear Cache": "캐시 지우기"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "Belum ada tandabuku",
"Importing...": "Mengimport...",
"Swipe for Brightness": "Leret untuk kecerahan",
"Slide along the left edge": "Leret di sepanjang tepi kiri"
"Slide along the left edge": "Leret di sepanjang tepi kiri",
"Failed to read cache": "Gagal membaca cache",
"Failed to clear cache": "Gagal mengosongkan cache",
"Calculating cache size...": "Mengira saiz cache…",
"Cache cleared": "Cache dikosongkan",
"{{count}} files_other": "{{count}} fail",
"Manage Cache": "Urus Cache",
"Deleting: {{file}}": "Memadam: {{file}}",
"Clearing cache...": "Mengosongkan cache…",
"This will delete all cached files. This cannot be undone.": "Tindakan ini akan memadam semua fail cache. Tindakan ini tidak boleh diundurkan.",
"Confirm Clear": "Sahkan Pengosongan",
"Clearing...": "Mengosongkan…",
"Clear Cache": "Kosongkan Cache"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Nog geen bladwijzer",
"Importing...": "Bezig met importeren...",
"Swipe for Brightness": "Veeg voor helderheid",
"Slide along the left edge": "Veeg langs de linkerrand"
"Slide along the left edge": "Veeg langs de linkerrand",
"Failed to read cache": "Kan cache niet lezen",
"Failed to clear cache": "Kan cache niet wissen",
"Calculating cache size...": "Cachegrootte berekenen…",
"Cache cleared": "Cache gewist",
"{{count}} files_one": "{{count}} bestand",
"{{count}} files_other": "{{count}} bestanden",
"Manage Cache": "Cache beheren",
"Deleting: {{file}}": "Verwijderen: {{file}}",
"Clearing cache...": "Cache wissen…",
"This will delete all cached files. This cannot be undone.": "Hiermee worden alle gecachte bestanden verwijderd. Dit kan niet ongedaan worden gemaakt.",
"Confirm Clear": "Wissen bevestigen",
"Clearing...": "Bezig met wissen…",
"Clear Cache": "Cache wissen"
}
@@ -1743,5 +1743,20 @@
"No bookmark yet": "Brak zakładek",
"Importing...": "Importowanie...",
"Swipe for Brightness": "Przesuń, aby zmienić jasność",
"Slide along the left edge": "Przesuń wzdłuż lewej krawędzi"
"Slide along the left edge": "Przesuń wzdłuż lewej krawędzi",
"Failed to read cache": "Nie udało się odczytać pamięci podręcznej",
"Failed to clear cache": "Nie udało się wyczyścić pamięci podręcznej",
"Calculating cache size...": "Obliczanie rozmiaru pamięci podręcznej…",
"Cache cleared": "Wyczyszczono pamięć podręczną",
"{{count}} files_one": "{{count}} plik",
"{{count}} files_few": "{{count}} pliki",
"{{count}} files_many": "{{count}} plików",
"{{count}} files_other": "{{count}} pliku",
"Manage Cache": "Zarządzaj pamięcią podręczną",
"Deleting: {{file}}": "Usuwanie: {{file}}",
"Clearing cache...": "Czyszczenie pamięci podręcznej…",
"This will delete all cached files. This cannot be undone.": "Spowoduje to usunięcie wszystkich plików z pamięci podręcznej. Tej operacji nie można cofnąć.",
"Confirm Clear": "Potwierdź czyszczenie",
"Clearing...": "Czyszczenie…",
"Clear Cache": "Wyczyść pamięć podręczną"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Nenhum marcador ainda",
"Importing...": "Importando...",
"Swipe for Brightness": "Deslizar para o brilho",
"Slide along the left edge": "Deslize pela borda esquerda"
"Slide along the left edge": "Deslize pela borda esquerda",
"Failed to read cache": "Falha ao ler o cache",
"Failed to clear cache": "Falha ao limpar o cache",
"Calculating cache size...": "Calculando o tamanho do cache…",
"Cache cleared": "Cache limpo",
"{{count}} files_one": "{{count}} arquivo",
"{{count}} files_many": "{{count}} arquivos",
"{{count}} files_other": "{{count}} arquivos",
"Manage Cache": "Gerenciar cache",
"Deleting: {{file}}": "Excluindo: {{file}}",
"Clearing cache...": "Limpando o cache…",
"This will delete all cached files. This cannot be undone.": "Isso excluirá todos os arquivos em cache. Esta ação não pode ser desfeita.",
"Confirm Clear": "Confirmar limpeza",
"Clearing...": "Limpando…",
"Clear Cache": "Limpar cache"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Ainda não há marcadores",
"Importing...": "Importando...",
"Swipe for Brightness": "Deslizar para o brilho",
"Slide along the left edge": "Deslize pela borda esquerda"
"Slide along the left edge": "Deslize pela borda esquerda",
"Failed to read cache": "Falha ao ler a cache",
"Failed to clear cache": "Falha ao limpar a cache",
"Calculating cache size...": "A calcular o tamanho da cache…",
"Cache cleared": "Cache limpa",
"{{count}} files_one": "{{count}} ficheiro",
"{{count}} files_many": "{{count}} ficheiros",
"{{count}} files_other": "{{count}} ficheiros",
"Manage Cache": "Gerir cache",
"Deleting: {{file}}": "A eliminar: {{file}}",
"Clearing cache...": "A limpar a cache…",
"This will delete all cached files. This cannot be undone.": "Isto irá eliminar todos os ficheiros em cache. Esta ação não pode ser anulada.",
"Confirm Clear": "Confirmar limpeza",
"Clearing...": "A limpar…",
"Clear Cache": "Limpar cache"
}
@@ -1711,5 +1711,19 @@
"No bookmark yet": "Niciun marcaj încă",
"Importing...": "Se importă...",
"Swipe for Brightness": "Glisează pentru luminozitate",
"Slide along the left edge": "Glisează de-a lungul marginii stângi"
"Slide along the left edge": "Glisează de-a lungul marginii stângi",
"Failed to read cache": "Citirea memoriei cache a eșuat",
"Failed to clear cache": "Golirea memoriei cache a eșuat",
"Calculating cache size...": "Se calculează dimensiunea memoriei cache…",
"Cache cleared": "Memoria cache a fost golită",
"{{count}} files_one": "{{count}} fișier",
"{{count}} files_few": "{{count}} fișiere",
"{{count}} files_other": "{{count}} de fișiere",
"Manage Cache": "Gestionează memoria cache",
"Deleting: {{file}}": "Se șterge: {{file}}",
"Clearing cache...": "Se golește memoria cache…",
"This will delete all cached files. This cannot be undone.": "Această acțiune va șterge toate fișierele din memoria cache. Acțiunea nu poate fi anulată.",
"Confirm Clear": "Confirmă golirea",
"Clearing...": "Se golește…",
"Clear Cache": "Golește memoria cache"
}
@@ -1743,5 +1743,20 @@
"No bookmark yet": "Пока нет закладок",
"Importing...": "Импорт...",
"Swipe for Brightness": "Свайп для яркости",
"Slide along the left edge": "Проведите вдоль левого края"
"Slide along the left edge": "Проведите вдоль левого края",
"Failed to read cache": "Не удалось прочитать кэш",
"Failed to clear cache": "Не удалось очистить кэш",
"Calculating cache size...": "Вычисление размера кэша…",
"Cache cleared": "Кэш очищен",
"{{count}} files_one": "{{count}} файл",
"{{count}} files_few": "{{count}} файла",
"{{count}} files_many": "{{count}} файлов",
"{{count}} files_other": "{{count}} файла",
"Manage Cache": "Управление кэшем",
"Deleting: {{file}}": "Удаление: {{file}}",
"Clearing cache...": "Очистка кэша…",
"This will delete all cached files. This cannot be undone.": "Это удалит все файлы из кэша. Это действие нельзя отменить.",
"Confirm Clear": "Подтвердить очистку",
"Clearing...": "Очистка…",
"Clear Cache": "Очистить кэш"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "තවම පොත් සලකුණක් නැත",
"Importing...": "ආයාත වෙමින්...",
"Swipe for Brightness": "දීප්තිය සඳහා ස්වයිප් කරන්න",
"Slide along the left edge": "වම් කෙළවර දිගේ අදින්න"
"Slide along the left edge": "වම් කෙළවර දිගේ අදින්න",
"Failed to read cache": "හැඹිලිය කියවීමට අසමත් විය",
"Failed to clear cache": "හැඹිලිය හිස් කිරීමට අසමත් විය",
"Calculating cache size...": "හැඹිලි ප්‍රමාණය ගණනය කරමින්…",
"Cache cleared": "හැඹිලිය හිස් කරන ලදී",
"{{count}} files_one": "ගොනු {{count}}",
"{{count}} files_other": "ගොනු {{count}}",
"Manage Cache": "හැඹිලිය කළමනාකරණය",
"Deleting: {{file}}": "මකමින්: {{file}}",
"Clearing cache...": "හැඹිලිය හිස් කරමින්…",
"This will delete all cached files. This cannot be undone.": "මෙය සියලුම හැඹිලිගත ගොනු මකා දමයි. මෙය පෙරළිය නොහැක.",
"Confirm Clear": "හිස් කිරීම තහවුරු කරන්න",
"Clearing...": "හිස් කරමින්…",
"Clear Cache": "හැඹිලිය හිස් කරන්න"
}
@@ -1743,5 +1743,20 @@
"No bookmark yet": "Še ni zaznamka",
"Importing...": "Uvažanje ...",
"Swipe for Brightness": "Poteg za svetlost",
"Slide along the left edge": "Povlecite ob levem robu"
"Slide along the left edge": "Povlecite ob levem robu",
"Failed to read cache": "Predpomnilnika ni bilo mogoče prebrati",
"Failed to clear cache": "Predpomnilnika ni bilo mogoče počistiti",
"Calculating cache size...": "Računanje velikosti predpomnilnika…",
"Cache cleared": "Predpomnilnik počiščen",
"{{count}} files_one": "{{count}} datoteka",
"{{count}} files_two": "{{count}} datoteki",
"{{count}} files_few": "{{count}} datoteke",
"{{count}} files_other": "{{count}} datotek",
"Manage Cache": "Upravljanje predpomnilnika",
"Deleting: {{file}}": "Brisanje: {{file}}",
"Clearing cache...": "Čiščenje predpomnilnika…",
"This will delete all cached files. This cannot be undone.": "To bo izbrisalo vse predpomnjene datoteke. Tega dejanja ni mogoče razveljaviti.",
"Confirm Clear": "Potrdi čiščenje",
"Clearing...": "Čiščenje…",
"Clear Cache": "Počisti predpomnilnik"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Inget bokmärke än",
"Importing...": "Importerar...",
"Swipe for Brightness": "Svep för ljusstyrka",
"Slide along the left edge": "Dra längs vänsterkanten"
"Slide along the left edge": "Dra längs vänsterkanten",
"Failed to read cache": "Det gick inte att läsa cachen",
"Failed to clear cache": "Det gick inte att rensa cachen",
"Calculating cache size...": "Beräknar cachestorlek…",
"Cache cleared": "Cachen rensad",
"{{count}} files_one": "{{count}} fil",
"{{count}} files_other": "{{count}} filer",
"Manage Cache": "Hantera cache",
"Deleting: {{file}}": "Tar bort: {{file}}",
"Clearing cache...": "Rensar cache…",
"This will delete all cached files. This cannot be undone.": "Detta tar bort alla cachade filer. Åtgärden kan inte ångras.",
"Confirm Clear": "Bekräfta rensning",
"Clearing...": "Rensar…",
"Clear Cache": "Rensa cache"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "இன்னும் புக்மார்க் இல்லை",
"Importing...": "இறக்குமதி செய்கிறது...",
"Swipe for Brightness": "ஒளிர்வுக்கு ஸ்வைப் செய்யவும்",
"Slide along the left edge": "இடது விளிம்பில் இழுக்கவும்"
"Slide along the left edge": "இடது விளிம்பில் இழுக்கவும்",
"Failed to read cache": "தற்காலிக சேமிப்பைப் படிக்க முடியவில்லை",
"Failed to clear cache": "தற்காலிக சேமிப்பை அழிக்க முடியவில்லை",
"Calculating cache size...": "தற்காலிக சேமிப்பின் அளவு கணக்கிடப்படுகிறது…",
"Cache cleared": "தற்காலிக சேமிப்பு அழிக்கப்பட்டது",
"{{count}} files_one": "{{count}} கோப்பு",
"{{count}} files_other": "{{count}} கோப்புகள்",
"Manage Cache": "தற்காலிக சேமிப்பை நிர்வகி",
"Deleting: {{file}}": "நீக்கப்படுகிறது: {{file}}",
"Clearing cache...": "தற்காலிக சேமிப்பு அழிக்கப்படுகிறது…",
"This will delete all cached files. This cannot be undone.": "இது தற்காலிகச் சேமிப்பில் உள்ள அனைத்துக் கோப்புகளையும் நீக்கும். இதை மீட்டெடுக்க முடியாது.",
"Confirm Clear": "அழிப்பதை உறுதிப்படுத்து",
"Clearing...": "அழிக்கப்படுகிறது…",
"Clear Cache": "தற்காலிக சேமிப்பை அழி"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "ยังไม่มีบุ๊กมาร์ก",
"Importing...": "กำลังนำเข้า...",
"Swipe for Brightness": "ปัดเพื่อปรับความสว่าง",
"Slide along the left edge": "เลื่อนตามขอบด้านซ้าย"
"Slide along the left edge": "เลื่อนตามขอบด้านซ้าย",
"Failed to read cache": "อ่านแคชไม่สำเร็จ",
"Failed to clear cache": "ล้างแคชไม่สำเร็จ",
"Calculating cache size...": "กำลังคำนวณขนาดแคช…",
"Cache cleared": "ล้างแคชแล้ว",
"{{count}} files_other": "{{count}} ไฟล์",
"Manage Cache": "จัดการแคช",
"Deleting: {{file}}": "กำลังลบ: {{file}}",
"Clearing cache...": "กำลังล้างแคช…",
"This will delete all cached files. This cannot be undone.": "การดำเนินการนี้จะลบไฟล์แคชทั้งหมด ไม่สามารถยกเลิกได้",
"Confirm Clear": "ยืนยันการล้าง",
"Clearing...": "กำลังล้าง…",
"Clear Cache": "ล้างแคช"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Henüz yer imi yok",
"Importing...": "İçe aktarılıyor...",
"Swipe for Brightness": "Parlaklık için kaydır",
"Slide along the left edge": "Sol kenar boyunca kaydır"
"Slide along the left edge": "Sol kenar boyunca kaydır",
"Failed to read cache": "Önbellek okunamadı",
"Failed to clear cache": "Önbellek temizlenemedi",
"Calculating cache size...": "Önbellek boyutu hesaplanıyor…",
"Cache cleared": "Önbellek temizlendi",
"{{count}} files_one": "{{count}} dosya",
"{{count}} files_other": "{{count}} dosya",
"Manage Cache": "Önbelleği Yönet",
"Deleting: {{file}}": "Siliniyor: {{file}}",
"Clearing cache...": "Önbellek temizleniyor…",
"This will delete all cached files. This cannot be undone.": "Bu, önbelleğe alınmış tüm dosyaları siler. Bu işlem geri alınamaz.",
"Confirm Clear": "Temizlemeyi Onayla",
"Clearing...": "Temizleniyor…",
"Clear Cache": "Önbelleği Temizle"
}
@@ -1743,5 +1743,20 @@
"No bookmark yet": "Поки немає закладок",
"Importing...": "Імпорт...",
"Swipe for Brightness": "Свайп для яскравості",
"Slide along the left edge": "Проведіть уздовж лівого краю"
"Slide along the left edge": "Проведіть уздовж лівого краю",
"Failed to read cache": "Не вдалося прочитати кеш",
"Failed to clear cache": "Не вдалося очистити кеш",
"Calculating cache size...": "Обчислення розміру кешу…",
"Cache cleared": "Кеш очищено",
"{{count}} files_one": "{{count}} файл",
"{{count}} files_few": "{{count}} файли",
"{{count}} files_many": "{{count}} файлів",
"{{count}} files_other": "{{count}} файлу",
"Manage Cache": "Керування кешем",
"Deleting: {{file}}": "Видалення: {{file}}",
"Clearing cache...": "Очищення кешу…",
"This will delete all cached files. This cannot be undone.": "Це видалить усі файли з кешу. Цю дію не можна скасувати.",
"Confirm Clear": "Підтвердити очищення",
"Clearing...": "Очищення…",
"Clear Cache": "Очистити кеш"
}
@@ -1679,5 +1679,18 @@
"No bookmark yet": "Hali xatchoʻp yoʻq",
"Importing...": "Import qilinmoqda...",
"Swipe for Brightness": "Yorqinlik uchun suring",
"Slide along the left edge": "Chap chekka bo'ylab suring"
"Slide along the left edge": "Chap chekka bo'ylab suring",
"Failed to read cache": "Keshni oqib bolmadi",
"Failed to clear cache": "Keshni tozalab bolmadi",
"Calculating cache size...": "Kesh hajmi hisoblanmoqda…",
"Cache cleared": "Kesh tozalandi",
"{{count}} files_one": "{{count}} ta fayl",
"{{count}} files_other": "{{count}} ta fayl",
"Manage Cache": "Keshni boshqarish",
"Deleting: {{file}}": "Ochirilmoqda: {{file}}",
"Clearing cache...": "Kesh tozalanmoqda…",
"This will delete all cached files. This cannot be undone.": "Bu barcha keshlangan fayllarni ochiradi. Buni qaytarib bolmaydi.",
"Confirm Clear": "Tozalashni tasdiqlash",
"Clearing...": "Tozalanmoqda…",
"Clear Cache": "Keshni tozalash"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "Chưa có dấu trang",
"Importing...": "Đang nhập...",
"Swipe for Brightness": "Vuốt để chỉnh độ sáng",
"Slide along the left edge": "Vuốt dọc theo cạnh trái"
"Slide along the left edge": "Vuốt dọc theo cạnh trái",
"Failed to read cache": "Không thể đọc bộ nhớ đệm",
"Failed to clear cache": "Không thể xóa bộ nhớ đệm",
"Calculating cache size...": "Đang tính kích thước bộ nhớ đệm…",
"Cache cleared": "Đã xóa bộ nhớ đệm",
"{{count}} files_other": "{{count}} tệp",
"Manage Cache": "Quản lý bộ nhớ đệm",
"Deleting: {{file}}": "Đang xóa: {{file}}",
"Clearing cache...": "Đang xóa bộ nhớ đệm…",
"This will delete all cached files. This cannot be undone.": "Thao tác này sẽ xóa tất cả tệp trong bộ nhớ đệm. Không thể hoàn tác.",
"Confirm Clear": "Xác nhận xóa",
"Clearing...": "Đang xóa…",
"Clear Cache": "Xóa bộ nhớ đệm"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "尚无书签",
"Importing...": "正在导入...",
"Swipe for Brightness": "滑动调节亮度",
"Slide along the left edge": "沿左侧边缘滑动"
"Slide along the left edge": "沿左侧边缘滑动",
"Failed to read cache": "读取缓存失败",
"Failed to clear cache": "清除缓存失败",
"Calculating cache size...": "正在计算缓存大小…",
"Cache cleared": "缓存已清除",
"{{count}} files_other": "{{count}} 个文件",
"Manage Cache": "管理缓存",
"Deleting: {{file}}": "正在删除:{{file}}",
"Clearing cache...": "正在清除缓存…",
"This will delete all cached files. This cannot be undone.": "这将删除所有缓存文件,此操作无法撤销。",
"Confirm Clear": "确认清除",
"Clearing...": "正在清除…",
"Clear Cache": "清除缓存"
}
@@ -1647,5 +1647,17 @@
"No bookmark yet": "尚無書籤",
"Importing...": "正在匯入...",
"Swipe for Brightness": "滑動調整亮度",
"Slide along the left edge": "沿左側邊緣滑動"
"Slide along the left edge": "沿左側邊緣滑動",
"Failed to read cache": "讀取快取失敗",
"Failed to clear cache": "清除快取失敗",
"Calculating cache size...": "正在計算快取大小…",
"Cache cleared": "快取已清除",
"{{count}} files_other": "{{count}} 個檔案",
"Manage Cache": "管理快取",
"Deleting: {{file}}": "正在刪除:{{file}}",
"Clearing cache...": "正在清除快取…",
"This will delete all cached files. This cannot be undone.": "這將刪除所有快取檔案,此操作無法復原。",
"Confirm Clear": "確認清除",
"Clearing...": "正在清除…",
"Clear Cache": "清除快取"
}
@@ -0,0 +1,114 @@
import { describe, it, expect, vi } from 'vitest';
import { AppService, FileItem } from '@/types/system';
import {
clearCacheEntries,
getCacheEntries,
getCacheStats,
CacheClearProgress,
CacheEntry,
} from '@/utils/cache';
const makeFiles = (...names: string[]): FileItem[] =>
names.map((path, i) => ({ path, size: (i + 1) * 10 }));
describe('getCacheStats', () => {
it('sums file count and byte size', () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a', size: 10 },
{ base: 'Cache', path: 'b', size: 20 },
{ base: 'None', path: '/Inbox/c', size: 30 },
];
expect(getCacheStats(entries)).toEqual({ count: 3, size: 60 });
});
it('returns zeros for an empty cache', () => {
expect(getCacheStats([])).toEqual({ count: 0, size: 0 });
});
});
describe('getCacheEntries', () => {
it('reads the Cache base root with base-relative paths', async () => {
const readDirectory = vi.fn().mockResolvedValue(makeFiles('x.json', 'y.epub'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [{ base: 'Cache', dir: '' }]);
expect(readDirectory).toHaveBeenCalledWith('', 'Cache');
expect(entries).toEqual([
{ base: 'Cache', path: 'x.json', size: 10 },
{ base: 'Cache', path: 'y.epub', size: 20 },
]);
});
it('prefixes a non-root source dir so paths are directly deletable (iOS Inbox)', async () => {
const readDirectory = vi.fn().mockResolvedValue(makeFiles('book.epub'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [
{ base: 'None', dir: '/var/mobile/.../Documents/Inbox' },
]);
expect(readDirectory).toHaveBeenCalledWith('/var/mobile/.../Documents/Inbox', 'None');
expect(entries).toEqual([
{ base: 'None', path: '/var/mobile/.../Documents/Inbox/book.epub', size: 10 },
]);
});
it('merges multiple sources and skips unreadable ones', async () => {
const readDirectory = vi
.fn()
.mockResolvedValueOnce(makeFiles('cache.json'))
.mockRejectedValueOnce(new Error('no inbox'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [
{ base: 'Cache', dir: '' },
{ base: 'None', dir: '/Inbox' },
]);
expect(entries).toEqual([{ base: 'Cache', path: 'cache.json', size: 10 }]);
});
});
describe('clearCacheEntries', () => {
it('deletes every entry with its own base and reports progress', async () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a.json', size: 10 },
{ base: 'Cache', path: 'b.epub', size: 20 },
{ base: 'None', path: '/Inbox/c.epub', size: 30 },
];
const deleteFile = vi.fn().mockResolvedValue(undefined);
const appService = { deleteFile } as unknown as AppService;
const progress: CacheClearProgress[] = [];
const result = await clearCacheEntries(appService, entries, (p) => progress.push(p));
expect(result).toEqual({ deleted: 3, failed: 0 });
expect(deleteFile).toHaveBeenCalledWith('a.json', 'Cache');
expect(deleteFile).toHaveBeenCalledWith('/Inbox/c.epub', 'None');
expect(progress).toEqual([
{ current: 1, total: 3, currentFile: 'a.json' },
{ current: 2, total: 3, currentFile: 'b.epub' },
{ current: 3, total: 3, currentFile: '/Inbox/c.epub' },
]);
});
it('counts failures without aborting the loop', async () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a', size: 1 },
{ base: 'Cache', path: 'b', size: 1 },
{ base: 'Cache', path: 'c', size: 1 },
];
const deleteFile = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('locked'))
.mockResolvedValueOnce(undefined);
const appService = { deleteFile } as unknown as AppService;
const result = await clearCacheEntries(appService, entries);
expect(result).toEqual({ deleted: 2, failed: 1 });
expect(deleteFile).toHaveBeenCalledTimes(3);
});
});
@@ -0,0 +1,261 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import {
RiDatabase2Line,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoader2Line,
} from 'react-icons/ri';
import { documentDir, join } from '@tauri-apps/api/path';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { formatBytes } from '@/utils/book';
import {
clearCacheEntries,
getCacheEntries,
getCacheStats,
CacheClearProgress,
CacheSource,
} from '@/utils/cache';
import { AppService } from '@/types/system';
import Dialog from '@/components/Dialog';
export const setCacheManagerDialogVisible = (visible: boolean) => {
const dialog = document.getElementById('cache_manager_window');
if (dialog) {
const event = new CustomEvent('setDialogVisibility', {
detail: { visible },
});
dialog.dispatchEvent(event);
}
};
type CacheStatus = 'scanning' | 'idle' | 'confirming' | 'clearing' | 'done' | 'error';
/**
* Locations Manage Cache clears (mobile only). Both iOS and Android clear the
* app Cache and Temp bases; iOS additionally clears the `Documents/Inbox`
* folder, where "Open in Readest" leaves already-imported book copies that
* otherwise linger forever.
*/
const getCacheSources = async (appService: AppService): Promise<CacheSource[]> => {
const sources: CacheSource[] = [
{ base: 'Cache', dir: '' },
{ base: 'Temp', dir: '' },
];
if (appService.isIOSApp) {
try {
const inboxDir = await join(await documentDir(), 'Inbox');
sources.push({ base: 'None', dir: inboxDir });
} catch {
// Documents dir unavailable — skip the inbox source.
}
}
return sources;
};
export const CacheManagerWindow = () => {
const _ = useTranslation();
const { appService } = useEnv();
const [isOpen, setIsOpen] = useState(false);
const [status, setStatus] = useState<CacheStatus>('scanning');
const [count, setCount] = useState(0);
const [size, setSize] = useState(0);
const [progress, setProgress] = useState<CacheClearProgress>({ current: 0, total: 0 });
const [errorMessage, setErrorMessage] = useState('');
const scanCache = async () => {
if (!appService) return;
setStatus('scanning');
setErrorMessage('');
try {
const entries = await getCacheEntries(appService, await getCacheSources(appService));
const stats = getCacheStats(entries);
setCount(stats.count);
setSize(stats.size);
setStatus('idle');
} catch (error) {
console.error('Error scanning cache:', error);
setErrorMessage(_('Failed to read cache'));
setStatus('error');
}
};
useEffect(() => {
const handleCustomEvent = (event: CustomEvent) => {
setIsOpen(event.detail.visible);
if (event.detail.visible) {
scanCache();
}
};
const el = document.getElementById('cache_manager_window');
if (el) {
el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
}
return () => {
if (el) {
el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService]);
const handleClearClick = () => {
if (status === 'idle' && count > 0) {
setStatus('confirming');
}
};
const handleConfirmClear = async () => {
if (!appService) return;
setStatus('clearing');
setProgress({ current: 0, total: 0 });
try {
const entries = await getCacheEntries(appService, await getCacheSources(appService));
await clearCacheEntries(appService, entries, setProgress);
await scanCache();
setStatus('done');
} catch (error) {
console.error('Error clearing cache:', error);
setErrorMessage(_('Failed to clear cache'));
setStatus('error');
}
};
const handleClose = () => {
if (status === 'clearing') {
return;
}
setIsOpen(false);
setStatus('scanning');
setProgress({ current: 0, total: 0 });
setErrorMessage('');
};
const progressPercentage =
progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
const primaryBtn = 'btn btn-contrast h-11 min-h-0 rounded-xl text-sm font-medium';
const ghostBtn = clsx(
'eink-bordered flex h-11 items-center justify-center rounded-xl border border-transparent',
'text-base-content hover:bg-base-200 text-sm font-medium transition-colors',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'disabled:opacity-40',
);
const heroIcon =
status === 'scanning' || status === 'clearing' ? (
<RiLoader2Line className='h-7 w-7 animate-spin' />
) : status === 'done' ? (
<RiCheckboxCircleFill className='text-success h-8 w-8' />
) : status === 'error' ? (
<RiErrorWarningFill className='text-error h-7 w-7' />
) : (
<RiDatabase2Line className='h-7 w-7' />
);
const heroCaption =
status === 'scanning'
? _('Calculating cache size...')
: status === 'done'
? _('Cache cleared')
: status === 'error'
? errorMessage
: _('{{count}} files', { count });
return (
<Dialog
id='cache_manager_window'
isOpen={isOpen}
title={_('Manage Cache')}
onClose={handleClose}
boxClassName='sm:!w-[440px] sm:!max-w-screen-sm sm:h-auto'
>
{isOpen && (
<div className='cache-manager-content flex flex-col gap-7 px-2 pb-2 pt-3'>
{/* Hero stat */}
<div className='flex flex-col items-center gap-3 text-center'>
<div
className='eink-bordered bg-base-200 text-base-content/80 flex h-16 w-16 items-center justify-center rounded-full'
aria-hidden='true'
>
{heroIcon}
</div>
<div className='flex flex-col items-center gap-1'>
<span className='text-base-content text-3xl font-bold tracking-tight tabular-nums'>
{status === 'scanning' ? '—' : formatBytes(size)}
</span>
<span className='text-base-content/60 line-clamp-2 text-sm'>{heroCaption}</span>
</div>
</div>
{/* Clearing progress */}
{status === 'clearing' && (
<div className='space-y-2'>
<div className='bg-base-200 h-1.5 w-full overflow-hidden rounded-full'>
<div
className='bg-base-content h-full rounded-full transition-all duration-300'
style={{ width: `${progressPercentage}%` }}
/>
</div>
<div className='text-base-content/55 flex items-center justify-between gap-3 text-xs'>
<span
className='overflow-hidden font-mono'
style={{
direction: 'rtl',
textAlign: 'left',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{progress.currentFile
? _('Deleting: {{file}}', { file: progress.currentFile })
: _('Clearing cache...')}
</span>
<span className='shrink-0 tabular-nums'>{progressPercentage}%</span>
</div>
</div>
)}
{/* Confirm notice */}
{status === 'confirming' && (
<p className='text-base-content/60 flex items-center justify-center gap-1.5 text-center text-[13px] leading-relaxed'>
<RiErrorWarningFill className='text-warning h-4 w-4 shrink-0' aria-hidden='true' />
{_('This will delete all cached files. This cannot be undone.')}
</p>
)}
{/* Action buttons */}
{status === 'done' ? (
<button className={primaryBtn} onClick={handleClose}>
{_('Close')}
</button>
) : (
<div className='grid grid-cols-2 gap-2.5'>
<button className={ghostBtn} onClick={handleClose} disabled={status === 'clearing'}>
{_('Cancel')}
</button>
{status === 'confirming' ? (
<button className={primaryBtn} onClick={handleConfirmClear}>
{_('Confirm Clear')}
</button>
) : (
<button
className={primaryBtn}
onClick={handleClearClick}
disabled={status !== 'idle' || count === 0}
>
{status === 'clearing' ? _('Clearing...') : _('Clear Cache')}
</button>
)}
</div>
)}
</div>
)}
</Dialog>
);
};
export default CacheManagerWindow;
@@ -10,6 +10,7 @@ import { invoke, PermissionState } from '@tauri-apps/api/core';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import { DOWNLOAD_READEST_URL } from '@/services/constants';
import { setBackupDialogVisible } from '@/app/library/components/BackupWindow';
import { setCacheManagerDialogVisible } from '@/app/library/components/CacheManagerWindow';
import { useAuth } from '@/context/AuthContext';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
@@ -192,6 +193,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
setBackupDialogVisible(true);
};
const handleManageCache = () => {
setIsDropdownOpen?.(false);
setCacheManagerDialogVisible(true);
};
const handleRefreshMetadata = async () => {
if (!appService || isRefreshingMetadata) return;
setIsRefreshingMetadata(true);
@@ -426,6 +432,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
onClick={handleRefreshMetadata}
disabled={isRefreshingMetadata}
/>
{appService?.isMobileApp && (
<MenuItem label={_('Manage Cache')} onClick={handleManageCache} />
)}
{!isPinEnabled && (
<MenuItem
label={_('Set PIN…')}
@@ -67,6 +67,7 @@ import { UpdaterWindow } from '@/components/UpdaterWindow';
import { CatalogDialog } from './components/OPDSDialog';
import { MigrateDataWindow } from './components/MigrateDataWindow';
import { BackupWindow } from './components/BackupWindow';
import { CacheManagerWindow } from './components/CacheManagerWindow';
import { useDragDropImport } from './hooks/useDragDropImport';
import { useTransferQueue } from '@/hooks/useTransferQueue';
import { useAppRouter } from '@/hooks/useAppRouter';
@@ -1468,6 +1469,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<UpdaterWindow />
<MigrateDataWindow />
<BackupWindow onPullLibrary={pullLibrary} />
<CacheManagerWindow />
{isSettingsDialogOpen && <SettingsDialog bookKey={''} />}
{showCatalogManager && <CatalogDialog onClose={handleDismissOPDSDialog} />}
{failedImportsModal && (
+84
View File
@@ -0,0 +1,84 @@
import { AppService, BaseDir } from '@/types/system';
export interface CacheClearProgress {
current: number;
total: number;
currentFile?: string;
}
export interface CacheClearResult {
deleted: number;
failed: number;
}
/** A cache location to scan and clear. */
export interface CacheSource {
base: BaseDir;
/** Directory within `base` to scan; '' for the base root. */
dir: string;
}
/** A single deletable file, with a path usable directly by deleteFile(path, base). */
export interface CacheEntry {
base: BaseDir;
path: string;
size: number;
}
/**
* List every file under the given cache sources as deletable entries. A source
* that can't be read (e.g. an Inbox that doesn't exist yet) simply contributes
* nothing instead of failing the whole scan.
*/
export const getCacheEntries = async (
appService: AppService,
sources: CacheSource[],
): Promise<CacheEntry[]> => {
const entries: CacheEntry[] = [];
for (const source of sources) {
try {
const files = await appService.readDirectory(source.dir, source.base);
for (const file of files) {
entries.push({
base: source.base,
path: source.dir ? `${source.dir}/${file.path}` : file.path,
size: file.size || 0,
});
}
} catch {
// Missing or unreadable source — skip it.
}
}
return entries;
};
/** Total file count and byte size for a set of cache entries. */
export const getCacheStats = (entries: CacheEntry[]): { count: number; size: number } => ({
count: entries.length,
size: entries.reduce((acc, entry) => acc + entry.size, 0),
});
/**
* Delete the given cache entries one at a time, reporting progress before each
* deletion. Individual failures are counted but never abort the loop, so a
* single locked file can't leave the cache half-cleared without feedback.
*/
export const clearCacheEntries = async (
appService: AppService,
entries: CacheEntry[],
onProgress?: (progress: CacheClearProgress) => void,
): Promise<CacheClearResult> => {
let deleted = 0;
let failed = 0;
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]!;
onProgress?.({ current: i + 1, total: entries.length, currentFile: entry.path });
try {
await appService.deleteFile(entry.path, entry.base);
deleted++;
} catch {
failed++;
}
}
return { deleted, failed };
};