From 42b47d73b7073c7962a5b37a6f2d3f9e6a8ff3e5 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 7 Dec 2025 03:25:40 +0800 Subject: [PATCH] feat: support cloud storage management (#2636) --- .../public/locales/ar/translation.json | 48 +- .../public/locales/bn/translation.json | 32 +- .../public/locales/bo/translation.json | 28 +- .../public/locales/de/translation.json | 32 +- .../public/locales/el/translation.json | 32 +- .../public/locales/en/translation.json | 10 +- .../public/locales/es/translation.json | 36 +- .../public/locales/fa/translation.json | 32 +- .../public/locales/fr/translation.json | 36 +- .../public/locales/hi/translation.json | 32 +- .../public/locales/id/translation.json | 28 +- .../public/locales/it/translation.json | 36 +- .../public/locales/ja/translation.json | 28 +- .../public/locales/ko/translation.json | 28 +- .../public/locales/ms/translation.json | 28 +- .../public/locales/nl/translation.json | 32 +- .../public/locales/pl/translation.json | 40 +- .../public/locales/pt/translation.json | 36 +- .../public/locales/ru/translation.json | 40 +- .../public/locales/si/translation.json | 32 +- .../public/locales/sv/translation.json | 32 +- .../public/locales/ta/translation.json | 32 +- .../public/locales/th/translation.json | 28 +- .../public/locales/tr/translation.json | 32 +- .../public/locales/uk/translation.json | 40 +- .../public/locales/vi/translation.json | 28 +- .../public/locales/zh-CN/translation.json | 28 +- .../public/locales/zh-TW/translation.json | 28 +- .../app/user/components/AccountActions.tsx | 10 + .../app/user/components/StorageManager.tsx | 570 ++++++++++++++++++ apps/readest-app/src/app/user/page.tsx | 57 +- apps/readest-app/src/libs/storage.ts | 119 ++++ .../readest-app/src/pages/api/storage/list.ts | 124 ++++ .../src/pages/api/storage/purge.ts | 146 +++++ .../src/pages/api/storage/stats.ts | 109 ++++ 35 files changed, 1981 insertions(+), 48 deletions(-) create mode 100644 apps/readest-app/src/app/user/components/StorageManager.tsx create mode 100644 apps/readest-app/src/pages/api/storage/list.ts create mode 100644 apps/readest-app/src/pages/api/storage/purge.ts create mode 100644 apps/readest-app/src/pages/api/storage/stats.ts diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 5aaa4da2..ce772fb7 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -749,5 +749,51 @@ "Enter {{terms}}": "أدخل {{terms}}", "No search results found": "لم يتم العثور على نتائج بحث", "Failed to load OPDS feed: {{status}} {{statusText}}": "فشل في تحميل تغذية OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "البحث في {{title}}" + "Search in {{title}}": "البحث في {{title}}", + "Manage Storage": "إدارة التخزين", + "Failed to load files": "فشل في تحميل الملفات", + "Deleted {{count}} file(s)_zero": "لم يتم حذف أي ملفات", + "Deleted {{count}} file(s)_one": "تم حذف ملف واحد", + "Deleted {{count}} file(s)_two": "تم حذف ملفين", + "Deleted {{count}} file(s)_few": "تم حذف {{count}} ملفات", + "Deleted {{count}} file(s)_many": "تم حذف {{count}} ملفًا", + "Deleted {{count}} file(s)_other": "تم حذف {{count}} ملف", + "Failed to delete {{count}} file(s)_zero": "فشل حذف أي ملفات", + "Failed to delete {{count}} file(s)_one": "فشل حذف ملف واحد", + "Failed to delete {{count}} file(s)_two": "فشل حذف ملفين", + "Failed to delete {{count}} file(s)_few": "فشل حذف {{count}} ملفات", + "Failed to delete {{count}} file(s)_many": "فشل حذف {{count}} ملفًا", + "Failed to delete {{count}} file(s)_other": "فشل حذف {{count}} ملف", + "Failed to delete files": "فشل حذف الملفات", + "Total Files": "إجمالي الملفات", + "Total Size": "إجمالي الحجم", + "Quota": "الحصة", + "Used": "المستخدم", + "Files": "الملفات", + "Search files...": "ابحث في الملفات...", + "Newest First": "الأحدث أولاً", + "Oldest First": "الأقدم أولاً", + "Largest First": "الأكبر أولاً", + "Smallest First": "الأصغر أولاً", + "Name A-Z": "الاسم من أ إلى ي", + "Name Z-A": "الاسم من ي إلى أ", + "{{count}} selected_zero": "لا يوجد عناصر محددة", + "{{count}} selected_one": "عنصر واحد محدد", + "{{count}} selected_two": "عنصران محددان", + "{{count}} selected_few": "{{count}} عناصر محددة", + "{{count}} selected_many": "{{count}} عنصرًا محددًا", + "{{count}} selected_other": "{{count}} عنصر محدد", + "Delete Selected": "حذف المحدد", + "Created": "تاريخ الإنشاء", + "No files found": "لا توجد ملفات", + "No files uploaded yet": "لم يتم رفع أي ملفات بعد", + "files": "الملفات", + "Page {{current}} of {{total}}": "الصفحة {{current}} من {{total}}", + "Are you sure to delete {{count}} selected file(s)?_zero": "هل أنت متأكد من حذف العناصر المحددة؟ (لا توجد عناصر)", + "Are you sure to delete {{count}} selected file(s)?_one": "هل أنت متأكد من حذف ملف واحد محدد؟", + "Are you sure to delete {{count}} selected file(s)?_two": "هل أنت متأكد من حذف ملفين محددين؟", + "Are you sure to delete {{count}} selected file(s)?_few": "هل أنت متأكد من حذف {{count}} ملفات محددة؟", + "Are you sure to delete {{count}} selected file(s)?_many": "هل أنت متأكد من حذف {{count}} ملفًا محددًا؟", + "Are you sure to delete {{count}} selected file(s)?_other": "هل أنت متأكد من حذف {{count}} ملف محدد؟", + "Cloud Storage Usage": "استخدام التخزين السحابي" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 02e4e31b..1007a9da 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "{{terms}} লিখুন", "No search results found": "কোনও অনুসন্ধান ফলাফল পাওয়া যায়নি", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS ফিড লোড করতে ব্যর্থ: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} এ অনুসন্ধান করুন" + "Search in {{title}}": "{{title}} এ অনুসন্ধান করুন", + "Manage Storage": "স্টোরেজ ম্যানেজমেন্ট", + "Failed to load files": "ফাইল লোড করতে ব্যর্থ", + "Deleted {{count}} file(s)_one": "{{count}}টি ফাইল মুছে ফেলা হয়েছে", + "Deleted {{count}} file(s)_other": "{{count}}টি ফাইল মুছে ফেলা হয়েছে", + "Failed to delete {{count}} file(s)_one": "{{count}}টি ফাইল মুছতে ব্যর্থ", + "Failed to delete {{count}} file(s)_other": "{{count}}টি ফাইল মুছতে ব্যর্থ", + "Failed to delete files": "ফাইল মুছতে ব্যর্থ", + "Total Files": "মোট ফাইল", + "Total Size": "মোট আকার", + "Quota": "কোটা", + "Used": "ব্যবহৃত", + "Files": "ফাইল", + "Search files...": "ফাইল খুঁজুন...", + "Newest First": "নতুন আগে", + "Oldest First": "পুরনো আগে", + "Largest First": "বড় আগে", + "Smallest First": "ছোট আগে", + "Name A-Z": "নাম A-Z", + "Name Z-A": "নাম Z-A", + "{{count}} selected_one": "{{count}}টি নির্বাচিত", + "{{count}} selected_other": "{{count}}টি নির্বাচিত", + "Delete Selected": "নির্বাচিত মুছুন", + "Created": "তৈরি হয়েছে", + "No files found": "কোনো ফাইল পাওয়া যায়নি", + "No files uploaded yet": "এখনও কোনো ফাইল আপলোড করা হয়নি", + "files": "ফাইল", + "Page {{current}} of {{total}}": "{{total}}টির মধ্যে {{current}} পৃষ্ঠা", + "Are you sure to delete {{count}} selected file(s)?_one": "আপনি কি নিশ্চিত যে {{count}}টি নির্বাচিত ফাইল মুছতে চান?", + "Are you sure to delete {{count}} selected file(s)?_other": "আপনি কি নিশ্চিত যে {{count}}টি নির্বাচিত ফাইল মুছতে চান?", + "Cloud Storage Usage": "ক্লাউড স্টোরেজ ব্যবহৃত" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 4e9fa41a..f53dc612 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "{{terms}} ལ་འགྲོ།", "No search results found": "འཚོལ་ཞིབ་རྫོགས་མ་ཐུབ།", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS འཕྲིན་འདེམས་བྱས་མ་ཐུབ།: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} ནང་འཚོལ།" + "Search in {{title}}": "{{title}} ནང་འཚོལ།", + "Manage Storage": "སྣོད་གསོག་དོ་དམ་བྱེད་པ", + "Failed to load files": "ཡིག་ཆ་སྣོན་པ་ཕམ་པ", + "Deleted {{count}} file(s)_other": "ཡིག་ཆ་ {{count}} བསུབས་ཟིན་པ", + "Failed to delete {{count}} file(s)_other": "ཡིག་ཆ་ {{count}} བསུབས་པ་ཕམ་པ", + "Failed to delete files": "ཡིག་ཆ་བསུབས་པ་ཕམ་པ", + "Total Files": "ཡིག་ཆ་ཡོངས་བསྡོམས", + "Total Size": "ཆེས་ཆེར་ཆེ་ཆུང", + "Quota": "ཁུལ་ཚད", + "Used": "ལག་ལེན་བྱས་ཟིན་པ", + "Files": "ཡིག་ཆ", + "Search files...": "ཡིག་ཆ་འཚོལ...", + "Newest First": "གསར་ཤོས་སྔོན་དུ", + "Oldest First": "རྙིང་ཤོས་སྔོན་དུ", + "Largest First": "ཆེ་ཤོས་སྔོན་དུ", + "Smallest First": "ཆུང་ཤོས་སྔོན་དུ", + "Name A-Z": "མིང་ A-Z", + "Name Z-A": "མིང་ Z-A", + "{{count}} selected_other": "{{count}} ཡིག་ཆ་འདེམས་ཟིན་པ", + "Delete Selected": "འདེམས་པ་བསུབས་པ", + "Created": "སྤེལ་བྱས་ཟིན་པ", + "No files found": "ཡིག་ཆ་མ་རྙེད་པ", + "No files uploaded yet": "ད་ཚུན་ཡིག་ཆ་སྣོན་མི་འདུག", + "files": "ཡིག་ཆ", + "Page {{current}} of {{total}}": "ཤོག་ངོས་ {{total}} ནས་ {{current}}", + "Are you sure to delete {{count}} selected file(s)?_other": "ཁྱེད་ཀྱིས་འདེམས་པའི་ཡིག་ཆ་ {{count}} བསུབས་དགོས་པ་ངེས་ཡིན་ན?", + "Cloud Storage Usage": "སྤྲིན་གནས་སྣོད་གསོག་ལུས་སྐོར།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index b92acf57..e22d6570 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "Gib {{terms}} ein", "No search results found": "Keine Suchergebnisse gefunden", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS-Feed konnte nicht geladen werden: {{status}} {{statusText}}", - "Search in {{title}}": "Suche in {{title}}" + "Search in {{title}}": "Suche in {{title}}", + "Manage Storage": "Speicher verwalten", + "Failed to load files": "Dateien konnten nicht geladen werden", + "Deleted {{count}} file(s)_one": "{{count}} Datei gelöscht", + "Deleted {{count}} file(s)_other": "{{count}} Dateien gelöscht", + "Failed to delete {{count}} file(s)_one": "Löschen von {{count}} Datei fehlgeschlagen", + "Failed to delete {{count}} file(s)_other": "Löschen von {{count}} Dateien fehlgeschlagen", + "Failed to delete files": "Dateien konnten nicht gelöscht werden", + "Total Files": "Gesamtdateien", + "Total Size": "Gesamtgröße", + "Quota": "Kontingent", + "Used": "Verwendet", + "Files": "Dateien", + "Search files...": "Dateien suchen...", + "Newest First": "Neueste zuerst", + "Oldest First": "Älteste zuerst", + "Largest First": "Größte zuerst", + "Smallest First": "Kleinste zuerst", + "Name A-Z": "Name A–Z", + "Name Z-A": "Name Z–A", + "{{count}} selected_one": "{{count}} ausgewählt", + "{{count}} selected_other": "{{count}} ausgewählt", + "Delete Selected": "Ausgewählte löschen", + "Created": "Erstellt", + "No files found": "Keine Dateien gefunden", + "No files uploaded yet": "Noch keine Dateien hochgeladen", + "files": "Dateien", + "Page {{current}} of {{total}}": "Seite {{current}} von {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Möchten Sie {{count}} ausgewählte Datei wirklich löschen?", + "Are you sure to delete {{count}} selected file(s)?_other": "Möchten Sie {{count}} ausgewählte Dateien wirklich löschen?", + "Cloud Storage Usage": "Cloud-Speichernutzung" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 58162e26..ec11face 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "Εισαγάγετε {{terms}}", "No search results found": "Δεν βρέθηκαν αποτελέσματα αναζήτησης", "Failed to load OPDS feed: {{status}} {{statusText}}": "Αποτυχία φόρτωσης ροής OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Αναζήτηση στο {{title}}" + "Search in {{title}}": "Αναζήτηση στο {{title}}", + "Manage Storage": "Διαχείριση αποθήκευσης", + "Failed to load files": "Αποτυχία φόρτωσης αρχείων", + "Deleted {{count}} file(s)_one": "Διαγράφηκε {{count}} αρχείο", + "Deleted {{count}} file(s)_other": "Διαγράφηκαν {{count}} αρχεία", + "Failed to delete {{count}} file(s)_one": "Αποτυχία διαγραφής {{count}} αρχείου", + "Failed to delete {{count}} file(s)_other": "Αποτυχία διαγραφής {{count}} αρχείων", + "Failed to delete files": "Αποτυχία διαγραφής αρχείων", + "Total Files": "Σύνολο αρχείων", + "Total Size": "Συνολικό μέγεθος", + "Quota": "Όριο", + "Used": "Χρησιμοποιήθηκε", + "Files": "Αρχεία", + "Search files...": "Αναζήτηση αρχείων...", + "Newest First": "Νεότερα πρώτα", + "Oldest First": "Παλαιότερα πρώτα", + "Largest First": "Μεγαλύτερα πρώτα", + "Smallest First": "Μικρότερα πρώτα", + "Name A-Z": "Όνομα A–Z", + "Name Z-A": "Όνομα Z–A", + "{{count}} selected_one": "{{count}} επιλεγμένο", + "{{count}} selected_other": "{{count}} επιλεγμένα", + "Delete Selected": "Διαγραφή επιλεγμένων", + "Created": "Δημιουργήθηκε", + "No files found": "Δεν βρέθηκαν αρχεία", + "No files uploaded yet": "Δεν έχουν ανέβει αρχεία ακόμη", + "files": "αρχεία", + "Page {{current}} of {{total}}": "Σελίδα {{current}} από {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Σίγουρα θέλετε να διαγράψετε {{count}} επιλεγμένο αρχείο;", + "Are you sure to delete {{count}} selected file(s)?_other": "Σίγουρα θέλετε να διαγράψετε {{count}} επιλεγμένα αρχεία;", + "Cloud Storage Usage": "Χρήση αποθήκευσης cloud" } diff --git a/apps/readest-app/public/locales/en/translation.json b/apps/readest-app/public/locales/en/translation.json index cfb61251..d9800817 100644 --- a/apps/readest-app/public/locales/en/translation.json +++ b/apps/readest-app/public/locales/en/translation.json @@ -9,5 +9,13 @@ "Search in {{count}} Book(s)..._one": "Search in {{count}} book...", "Search in {{count}} Book(s)..._other": "Search in {{count}} books...", "{{count}} pages left in chapter_one": "{{count}} page left in chapter", - "{{count}} pages left in chapter_other": "{{count}} pages left in chapter" + "{{count}} pages left in chapter_other": "{{count}} pages left in chapter", + "Deleted {{count}} file(s)_one": "Deleted {{count}} file", + "Deleted {{count}} file(s)_other": "Deleted {{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", + "{{count}} selected_one": "{{count}} selected", + "{{count}} selected_other": "{{count}} selected", + "Are you sure to delete {{count}} selected file(s)?_one": "Are you sure to delete {{count}} selected file?", + "Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 3929577c..20c15014 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -734,5 +734,39 @@ "Enter {{terms}}": "Ingrese {{terms}}", "No search results found": "No se encontraron resultados de búsqueda", "Failed to load OPDS feed: {{status}} {{statusText}}": "Error al cargar el feed OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Buscar en {{title}}" + "Search in {{title}}": "Buscar en {{title}}", + "Manage Storage": "Administrar almacenamiento", + "Failed to load files": "Error al cargar los archivos", + "Deleted {{count}} file(s)_one": "Se eliminó {{count}} archivo", + "Deleted {{count}} file(s)_many": "Se eliminaron {{count}} archivos", + "Deleted {{count}} file(s)_other": "Se eliminaron {{count}} archivos", + "Failed to delete {{count}} file(s)_one": "Error al eliminar {{count}} archivo", + "Failed to delete {{count}} file(s)_many": "Error al eliminar {{count}} archivos", + "Failed to delete {{count}} file(s)_other": "Error al eliminar {{count}} archivos", + "Failed to delete files": "Error al eliminar los archivos", + "Total Files": "Total de archivos", + "Total Size": "Tamaño total", + "Quota": "Cuota", + "Used": "Usado", + "Files": "Archivos", + "Search files...": "Buscar archivos...", + "Newest First": "Más nuevos primero", + "Oldest First": "Más antiguos primero", + "Largest First": "Más grandes primero", + "Smallest First": "Más pequeños primero", + "Name A-Z": "Nombre A–Z", + "Name Z-A": "Nombre Z–A", + "{{count}} selected_one": "{{count}} seleccionado", + "{{count}} selected_many": "{{count}} seleccionados", + "{{count}} selected_other": "{{count}} seleccionados", + "Delete Selected": "Eliminar seleccionados", + "Created": "Creado", + "No files found": "No se encontraron archivos", + "No files uploaded yet": "Aún no se han subido archivos", + "files": "archivos", + "Page {{current}} of {{total}}": "Página {{current}} de {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "¿Seguro que deseas eliminar {{count}} archivo seleccionado?", + "Are you sure to delete {{count}} selected file(s)?_many": "¿Seguro que deseas eliminar {{count}} archivos seleccionados?", + "Are you sure to delete {{count}} selected file(s)?_other": "¿Seguro que deseas eliminar {{count}} archivos seleccionados?", + "Cloud Storage Usage": "Uso de almacenamiento en la nube" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 36b59730..14ff5d8b 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "وارد کردن {{terms}}", "No search results found": "هیچ نتیجه‌ای یافت نشد", "Failed to load OPDS feed: {{status}} {{statusText}}": "بارگذاری فید OPDS ناموفق بود: {{status}} {{statusText}}", - "Search in {{title}}": "جستجو در {{title}}" + "Search in {{title}}": "جستجو در {{title}}", + "Manage Storage": "مدیریت فضای ذخیره‌سازی", + "Failed to load files": "بارگیری فایل‌ها ناموفق بود", + "Deleted {{count}} file(s)_one": "{{count}} فایل حذف شد", + "Deleted {{count}} file(s)_other": "{{count}} فایل حذف شدند", + "Failed to delete {{count}} file(s)_one": "حذف {{count}} فایل ناموفق بود", + "Failed to delete {{count}} file(s)_other": "حذف {{count}} فایل ناموفق بود", + "Failed to delete files": "حذف فایل‌ها ناموفق بود", + "Total Files": "کل فایل‌ها", + "Total Size": "اندازه کل", + "Quota": "سهمیه", + "Used": "استفاده‌شده", + "Files": "فایل‌ها", + "Search files...": "جستجوی فایل‌ها...", + "Newest First": "جدیدترین‌ها", + "Oldest First": "قدیمی‌ترین‌ها", + "Largest First": "بزرگ‌ترین‌ها", + "Smallest First": "کوچک‌ترین‌ها", + "Name A-Z": "نام A–Z", + "Name Z-A": "نام Z–A", + "{{count}} selected_one": "{{count}} مورد انتخاب شد", + "{{count}} selected_other": "{{count}} مورد انتخاب شدند", + "Delete Selected": "حذف موارد انتخاب‌شده", + "Created": "ایجاد شده", + "No files found": "هیچ فایلی پیدا نشد", + "No files uploaded yet": "هنوز فایلی بارگذاری نشده است", + "files": "فایل‌ها", + "Page {{current}} of {{total}}": "صفحه {{current}} از {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "آیا از حذف {{count}} فایل انتخاب‌شده مطمئن هستید؟", + "Are you sure to delete {{count}} selected file(s)?_other": "آیا از حذف {{count}} فایل انتخاب‌شده مطمئن هستید؟", + "Cloud Storage Usage": "استفاده از فضای ذخیره‌سازی ابری" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index fd46a5df..8b030ede 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -734,5 +734,39 @@ "Enter {{terms}}": "Entrez {{terms}}", "No search results found": "Aucun résultat trouvé", "Failed to load OPDS feed: {{status}} {{statusText}}": "Échec du chargement du flux OPDS : {{status}} {{statusText}}", - "Search in {{title}}": "Rechercher dans {{title}}" + "Search in {{title}}": "Rechercher dans {{title}}", + "Manage Storage": "Gérer le stockage", + "Failed to load files": "Échec du chargement des fichiers", + "Deleted {{count}} file(s)_one": "{{count}} fichier supprimé", + "Deleted {{count}} file(s)_many": "{{count}} fichiers supprimés", + "Deleted {{count}} file(s)_other": "{{count}} fichiers supprimés", + "Failed to delete {{count}} file(s)_one": "Échec de la suppression de {{count}} fichier", + "Failed to delete {{count}} file(s)_many": "Échec de la suppression de {{count}} fichiers", + "Failed to delete {{count}} file(s)_other": "Échec de la suppression de {{count}} fichiers", + "Failed to delete files": "Échec de la suppression des fichiers", + "Total Files": "Nombre total de fichiers", + "Total Size": "Taille totale", + "Quota": "Quota", + "Used": "Utilisé", + "Files": "Fichiers", + "Search files...": "Rechercher des fichiers...", + "Newest First": "Les plus récents", + "Oldest First": "Les plus anciens", + "Largest First": "Les plus volumineux", + "Smallest First": "Les moins volumineux", + "Name A-Z": "Nom A-Z", + "Name Z-A": "Nom Z-A", + "{{count}} selected_one": "{{count}} sélectionné", + "{{count}} selected_many": "{{count}} sélectionnés", + "{{count}} selected_other": "{{count}} sélectionnés", + "Delete Selected": "Supprimer la sélection", + "Created": "Créé", + "No files found": "Aucun fichier trouvé", + "No files uploaded yet": "Aucun fichier téléchargé pour le moment", + "files": "fichiers", + "Page {{current}} of {{total}}": "Page {{current}} sur {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Voulez-vous vraiment supprimer {{count}} fichier sélectionné ?", + "Are you sure to delete {{count}} selected file(s)?_many": "Voulez-vous vraiment supprimer {{count}} fichiers sélectionnés ?", + "Are you sure to delete {{count}} selected file(s)?_other": "Voulez-vous vraiment supprimer {{count}} fichiers sélectionnés ?", + "Cloud Storage Usage": "Utilisation du stockage cloud" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 449a2e35..cbf51f87 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "{{terms}} दर्ज करें", "No search results found": "कोई खोज परिणाम नहीं मिला", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS फ़ीड लोड करने में विफल: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} में खोजें" + "Search in {{title}}": "{{title}} में खोजें", + "Manage Storage": "स्टोरेज प्रबंधित करें", + "Failed to load files": "फ़ाइलें लोड करने में विफल", + "Deleted {{count}} file(s)_one": "{{count}} फ़ाइल हटाई गई", + "Deleted {{count}} file(s)_other": "{{count}} फ़ाइलें हटाई गईं", + "Failed to delete {{count}} file(s)_one": "{{count}} फ़ाइल हटाने में विफल", + "Failed to delete {{count}} file(s)_other": "{{count}} फ़ाइलें हटाने में विफल", + "Failed to delete files": "फ़ाइलें हटाने में विफल", + "Total Files": "कुल फ़ाइलें", + "Total Size": "कुल आकार", + "Quota": "कोटा", + "Used": "उपयोग किया गया", + "Files": "फ़ाइलें", + "Search files...": "फ़ाइलें खोजें...", + "Newest First": "नवीनतम पहले", + "Oldest First": "सबसे पुराने पहले", + "Largest First": "सबसे बड़ी पहले", + "Smallest First": "सबसे छोटी पहले", + "Name A-Z": "नाम A-Z", + "Name Z-A": "नाम Z-A", + "{{count}} selected_one": "{{count}} चयनित", + "{{count}} selected_other": "{{count}} चयनित", + "Delete Selected": "चयनित हटाएँ", + "Created": "बनाई गई", + "No files found": "कोई फ़ाइल नहीं मिली", + "No files uploaded yet": "अभी तक कोई फ़ाइल अपलोड नहीं की गई", + "files": "फ़ाइलें", + "Page {{current}} of {{total}}": "पृष्ठ {{current}} / {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "क्या आप वाकई {{count}} चयनित फ़ाइल हटाना चाहते हैं?", + "Are you sure to delete {{count}} selected file(s)?_other": "क्या आप वाकई {{count}} चयनित फ़ाइलें हटाना चाहते हैं?", + "Cloud Storage Usage": "क्लाउड स्टोरेज उपयोग" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 9ff9e457..3610908c 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "Masukkan {{terms}}", "No search results found": "Tidak ada hasil pencarian ditemukan", "Failed to load OPDS feed: {{status}} {{statusText}}": "Gagal memuat feed OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Cari di {{title}}" + "Search in {{title}}": "Cari di {{title}}", + "Manage Storage": "Kelola Penyimpanan", + "Failed to load files": "Gagal memuat file", + "Deleted {{count}} file(s)_other": "{{count}} file dihapus", + "Failed to delete {{count}} file(s)_other": "Gagal menghapus {{count}} file", + "Failed to delete files": "Gagal menghapus file", + "Total Files": "Total File", + "Total Size": "Total Ukuran", + "Quota": "Kuota", + "Used": "Digunakan", + "Files": "File", + "Search files...": "Cari file...", + "Newest First": "Terbaru", + "Oldest First": "Terlama", + "Largest First": "Terbesar", + "Smallest First": "Terkecil", + "Name A-Z": "Nama A-Z", + "Name Z-A": "Nama Z-A", + "{{count}} selected_other": "{{count}} dipilih", + "Delete Selected": "Hapus yang Dipilih", + "Created": "Dibuat", + "No files found": "Tidak ada file ditemukan", + "No files uploaded yet": "Belum ada file diunggah", + "files": "file", + "Page {{current}} of {{total}}": "Halaman {{current}} dari {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "Yakin ingin menghapus {{count}} file yang dipilih?", + "Cloud Storage Usage": "Penggunaan Penyimpanan Cloud" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 1797d525..2a28fb4c 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -734,5 +734,39 @@ "Enter {{terms}}": "Inserisci {{terms}}", "No search results found": "Nessun risultato di ricerca trovato", "Failed to load OPDS feed: {{status}} {{statusText}}": "Impossibile caricare il feed OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Cerca in {{title}}" + "Search in {{title}}": "Cerca in {{title}}", + "Manage Storage": "Gestisci archiviazione", + "Failed to load files": "Impossibile caricare i file", + "Deleted {{count}} file(s)_one": "È stato eliminato {{count}} file", + "Deleted {{count}} file(s)_many": "Sono stati eliminati {{count}} file", + "Deleted {{count}} file(s)_other": "Sono stati eliminati {{count}} file", + "Failed to delete {{count}} file(s)_one": "Impossibile eliminare {{count}} file", + "Failed to delete {{count}} file(s)_many": "Impossibile eliminare {{count}} file", + "Failed to delete {{count}} file(s)_other": "Impossibile eliminare {{count}} file", + "Failed to delete files": "Impossibile eliminare i file", + "Total Files": "File totali", + "Total Size": "Dimensione totale", + "Quota": "Quota", + "Used": "Utilizzato", + "Files": "File", + "Search files...": "Cerca file...", + "Newest First": "Più recenti", + "Oldest First": "Più vecchi", + "Largest First": "Più grandi", + "Smallest First": "Più piccoli", + "Name A-Z": "Nome A-Z", + "Name Z-A": "Nome Z-A", + "{{count}} selected_one": "{{count}} selezionato", + "{{count}} selected_many": "{{count}} selezionati", + "{{count}} selected_other": "{{count}} selezionati", + "Delete Selected": "Elimina selezionati", + "Created": "Creato", + "No files found": "Nessun file trovato", + "No files uploaded yet": "Nessun file caricato", + "files": "file", + "Page {{current}} of {{total}}": "Pagina {{current}} di {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Sei sicuro di voler eliminare {{count}} file selezionato?", + "Are you sure to delete {{count}} selected file(s)?_many": "Sei sicuro di voler eliminare {{count}} file selezionati?", + "Are you sure to delete {{count}} selected file(s)?_other": "Sei sicuro di voler eliminare {{count}} file selezionati?", + "Cloud Storage Usage": "Utilizzo archiviazione cloud" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index a2962b4b..3093dbf0 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "{{terms}}を入力してください", "No search results found": "検索結果が見つかりません", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDSフィードの読み込みに失敗しました: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}}内を検索" + "Search in {{title}}": "{{title}}内を検索", + "Manage Storage": "ストレージ管理", + "Failed to load files": "ファイルの読み込みに失敗しました", + "Deleted {{count}} file(s)_other": "{{count}} 件のファイルを削除しました", + "Failed to delete {{count}} file(s)_other": "{{count}} 件のファイルの削除に失敗しました", + "Failed to delete files": "ファイルの削除に失敗しました", + "Total Files": "ファイル合計", + "Total Size": "合計サイズ", + "Quota": "容量制限", + "Used": "使用済み", + "Files": "ファイル", + "Search files...": "ファイルを検索...", + "Newest First": "新しい順", + "Oldest First": "古い順", + "Largest First": "大きい順", + "Smallest First": "小さい順", + "Name A-Z": "名前 A-Z", + "Name Z-A": "名前 Z-A", + "{{count}} selected_other": "{{count}} 件選択済み", + "Delete Selected": "選択した項目を削除", + "Created": "作成日", + "No files found": "ファイルが見つかりません", + "No files uploaded yet": "まだファイルがアップロードされていません", + "files": "ファイル", + "Page {{current}} of {{total}}": "ページ {{current}} / {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "選択した {{count}} 件のファイルを削除してもよろしいですか?", + "Cloud Storage Usage": "クラウドストレージ使用量" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index e4c41b7f..0ae0481a 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "{{terms}} 입력", "No search results found": "검색 결과가 없습니다", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS 피드를 불러오지 못했습니다: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}}에서 검색" + "Search in {{title}}": "{{title}}에서 검색", + "Manage Storage": "저장소 관리", + "Failed to load files": "파일 로드 실패", + "Deleted {{count}} file(s)_other": "{{count}}개의 파일이 삭제되었습니다", + "Failed to delete {{count}} file(s)_other": "{{count}}개의 파일 삭제 실패", + "Failed to delete files": "파일 삭제 실패", + "Total Files": "총 파일", + "Total Size": "총 용량", + "Quota": "쿼터", + "Used": "사용됨", + "Files": "파일", + "Search files...": "파일 검색...", + "Newest First": "최신순", + "Oldest First": "오래된순", + "Largest First": "큰 파일순", + "Smallest First": "작은 파일순", + "Name A-Z": "이름 A-Z", + "Name Z-A": "이름 Z-A", + "{{count}} selected_other": "{{count}}개 선택됨", + "Delete Selected": "선택 삭제", + "Created": "생성됨", + "No files found": "파일이 없습니다", + "No files uploaded yet": "아직 업로드된 파일이 없습니다", + "files": "파일", + "Page {{current}} of {{total}}": "페이지 {{current}} / {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "선택한 {{count}}개의 파일을 삭제하시겠습니까?", + "Cloud Storage Usage": "클라우드 저장소 사용량" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index ed2f0b80..252fd8ff 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "Masukkan {{terms}}", "No search results found": "Tiada hasil carian ditemui", "Failed to load OPDS feed: {{status}} {{statusText}}": "Gagal memuatkan suapan OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Cari dalam {{title}}" + "Search in {{title}}": "Cari dalam {{title}}", + "Manage Storage": "Urus Penyimpanan", + "Failed to load files": "Gagal memuatkan fail", + "Deleted {{count}} file(s)_other": "{{count}} fail telah dipadam", + "Failed to delete {{count}} file(s)_other": "Gagal memadam {{count}} fail", + "Failed to delete files": "Gagal memadam fail", + "Total Files": "Jumlah Fail", + "Total Size": "Jumlah Saiz", + "Quota": "Kuota", + "Used": "Digunakan", + "Files": "Fail", + "Search files...": "Cari fail...", + "Newest First": "Terbaru dahulu", + "Oldest First": "Tertua dahulu", + "Largest First": "Terbesar dahulu", + "Smallest First": "Terkecil dahulu", + "Name A-Z": "Nama A-Z", + "Name Z-A": "Nama Z-A", + "{{count}} selected_other": "{{count}} dipilih", + "Delete Selected": "Padam Dipilih", + "Created": "Dicipta", + "No files found": "Tiada fail dijumpai", + "No files uploaded yet": "Belum ada fail dimuat naik", + "files": "fail", + "Page {{current}} of {{total}}": "Halaman {{current}} daripada {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "Adakah anda pasti mahu memadam {{count}} fail yang dipilih?", + "Cloud Storage Usage": "Penggunaan Storan Awan" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 3fe1e70e..4fe4990a 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "Voer {{terms}} in", "No search results found": "Geen zoekresultaten gevonden", "Failed to load OPDS feed: {{status}} {{statusText}}": "Het laden van de OPDS-feed is mislukt: {{status}} {{statusText}}", - "Search in {{title}}": "Zoeken in {{title}}" + "Search in {{title}}": "Zoeken in {{title}}", + "Manage Storage": "Opslag beheren", + "Failed to load files": "Bestanden laden mislukt", + "Deleted {{count}} file(s)_one": "{{count}} bestand verwijderd", + "Deleted {{count}} file(s)_other": "{{count}} bestanden verwijderd", + "Failed to delete {{count}} file(s)_one": "Kon {{count}} bestand niet verwijderen", + "Failed to delete {{count}} file(s)_other": "Kon {{count}} bestanden niet verwijderen", + "Failed to delete files": "Bestanden verwijderen mislukt", + "Total Files": "Totaal aantal bestanden", + "Total Size": "Totale grootte", + "Quota": "Quota", + "Used": "Gebruikt", + "Files": "Bestanden", + "Search files...": "Bestanden zoeken...", + "Newest First": "Nieuwste eerst", + "Oldest First": "Oudste eerst", + "Largest First": "Grootste eerst", + "Smallest First": "Kleinste eerst", + "Name A-Z": "Naam A-Z", + "Name Z-A": "Naam Z-A", + "{{count}} selected_one": "{{count}} geselecteerd", + "{{count}} selected_other": "{{count}} geselecteerd", + "Delete Selected": "Verwijder geselecteerde", + "Created": "Gemaakt", + "No files found": "Geen bestanden gevonden", + "No files uploaded yet": "Nog geen bestanden geüpload", + "files": "bestanden", + "Page {{current}} of {{total}}": "Pagina {{current}} van {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Weet je zeker dat je {{count}} geselecteerd bestand wilt verwijderen?", + "Are you sure to delete {{count}} selected file(s)?_other": "Weet je zeker dat je {{count}} geselecteerde bestanden wilt verwijderen?", + "Cloud Storage Usage": "Cloudopslaggebruik" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 4bb105a5..2419f310 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -739,5 +739,43 @@ "Enter {{terms}}": "Wprowadź {{terms}}", "No search results found": "Nie znaleziono wyników wyszukiwania", "Failed to load OPDS feed: {{status}} {{statusText}}": "Nie udało się załadować kanału OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Szukaj w {{title}}" + "Search in {{title}}": "Szukaj w {{title}}", + "Manage Storage": "Zarządzaj pamięcią", + "Failed to load files": "Nie udało się wczytać plików", + "Deleted {{count}} file(s)_one": "Usunięto {{count}} plik", + "Deleted {{count}} file(s)_few": "Usunięto {{count}} pliki", + "Deleted {{count}} file(s)_many": "Usunięto {{count}} plików", + "Deleted {{count}} file(s)_other": "Usunięto {{count}} pliku", + "Failed to delete {{count}} file(s)_one": "Nie udało się usunąć {{count}} pliku", + "Failed to delete {{count}} file(s)_few": "Nie udało się usunąć {{count}} plików", + "Failed to delete {{count}} file(s)_many": "Nie udało się usunąć {{count}} plików", + "Failed to delete {{count}} file(s)_other": "Nie udało się usunąć {{count}} pliku", + "Failed to delete files": "Nie udało się usunąć plików", + "Total Files": "Łączna liczba plików", + "Total Size": "Łączny rozmiar", + "Quota": "Limit", + "Used": "Użyto", + "Files": "Pliki", + "Search files...": "Szukaj plików...", + "Newest First": "Najnowsze najpierw", + "Oldest First": "Najstarsze najpierw", + "Largest First": "Największe najpierw", + "Smallest First": "Najmniejsze najpierw", + "Name A-Z": "Nazwa A-Z", + "Name Z-A": "Nazwa Z-A", + "{{count}} selected_one": "Wybrano {{count}} plik", + "{{count}} selected_few": "Wybrano {{count}} pliki", + "{{count}} selected_many": "Wybrano {{count}} plików", + "{{count}} selected_other": "Wybrano {{count}} pliku", + "Delete Selected": "Usuń wybrane", + "Created": "Utworzono", + "No files found": "Nie znaleziono plików", + "No files uploaded yet": "Nie przesłano jeszcze żadnych plików", + "files": "pliki", + "Page {{current}} of {{total}}": "Strona {{current}} z {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Czy na pewno chcesz usunąć {{count}} wybrany plik?", + "Are you sure to delete {{count}} selected file(s)?_few": "Czy na pewno chcesz usunąć {{count}} wybrane pliki?", + "Are you sure to delete {{count}} selected file(s)?_many": "Czy na pewno chcesz usunąć {{count}} wybranych plików?", + "Are you sure to delete {{count}} selected file(s)?_other": "Czy na pewno chcesz usunąć {{count}} wybrany plik?", + "Cloud Storage Usage": "Użycie pamięci w chmurze" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index f82ea184..3981c47a 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -734,5 +734,39 @@ "Enter {{terms}}": "Insira {{terms}}", "No search results found": "Nenhum resultado encontrado", "Failed to load OPDS feed: {{status}} {{statusText}}": "Falha ao carregar o feed OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Pesquisar em {{title}}" + "Search in {{title}}": "Pesquisar em {{title}}", + "Manage Storage": "Gerir Armazenamento", + "Failed to load files": "Falha ao carregar arquivos", + "Deleted {{count}} file(s)_one": "Apagado {{count}} ficheiro", + "Deleted {{count}} file(s)_many": "Apagados {{count}} ficheiros", + "Deleted {{count}} file(s)_other": "Apagado {{count}} ficheiro", + "Failed to delete {{count}} file(s)_one": "Falha ao apagar {{count}} ficheiro", + "Failed to delete {{count}} file(s)_many": "Falha ao apagar {{count}} ficheiros", + "Failed to delete {{count}} file(s)_other": "Falha ao apagar {{count}} ficheiro", + "Failed to delete files": "Falha ao apagar arquivos", + "Total Files": "Total de Arquivos", + "Total Size": "Tamanho Total", + "Quota": "Quota", + "Used": "Usado", + "Files": "Arquivos", + "Search files...": "Procurar arquivos...", + "Newest First": "Mais Recentes Primeiro", + "Oldest First": "Mais Antigos Primeiro", + "Largest First": "Maiores Primeiro", + "Smallest First": "Menores Primeiro", + "Name A-Z": "Nome A-Z", + "Name Z-A": "Nome Z-A", + "{{count}} selected_one": "{{count}} selecionado", + "{{count}} selected_many": "{{count}} selecionados", + "{{count}} selected_other": "{{count}} selecionado", + "Delete Selected": "Apagar Selecionados", + "Created": "Criado", + "No files found": "Nenhum arquivo encontrado", + "No files uploaded yet": "Nenhum arquivo enviado ainda", + "files": "arquivos", + "Page {{current}} of {{total}}": "Página {{current}} de {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Tem certeza de que deseja apagar {{count}} ficheiro selecionado?", + "Are you sure to delete {{count}} selected file(s)?_many": "Tem certeza de que deseja apagar {{count}} ficheiros selecionados?", + "Are you sure to delete {{count}} selected file(s)?_other": "Tem certeza de que deseja apagar {{count}} ficheiro selecionado?", + "Cloud Storage Usage": "Uso de Armazenamento na Nuvem" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index a4ec2873..0bb54b6f 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -739,5 +739,43 @@ "Enter {{terms}}": "Введите {{terms}}", "No search results found": "Результаты поиска не найдены", "Failed to load OPDS feed: {{status}} {{statusText}}": "Не удалось загрузить ленту OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Поиск в {{title}}" + "Search in {{title}}": "Поиск в {{title}}", + "Manage Storage": "Управление хранилищем", + "Failed to load files": "Не удалось загрузить файлы", + "Deleted {{count}} file(s)_one": "Удалён {{count}} файл", + "Deleted {{count}} file(s)_few": "Удалено {{count}} файла", + "Deleted {{count}} file(s)_many": "Удалено {{count}} файлов", + "Deleted {{count}} file(s)_other": "Удалён {{count}} файл", + "Failed to delete {{count}} file(s)_one": "Не удалось удалить {{count}} файл", + "Failed to delete {{count}} file(s)_few": "Не удалось удалить {{count}} файла", + "Failed to delete {{count}} file(s)_many": "Не удалось удалить {{count}} файлов", + "Failed to delete {{count}} file(s)_other": "Не удалось удалить {{count}} файл", + "Failed to delete files": "Не удалось удалить файлы", + "Total Files": "Всего файлов", + "Total Size": "Общий размер", + "Quota": "Квота", + "Used": "Использовано", + "Files": "Файлы", + "Search files...": "Поиск файлов...", + "Newest First": "Сначала новые", + "Oldest First": "Сначала старые", + "Largest First": "Сначала крупные", + "Smallest First": "Сначала мелкие", + "Name A-Z": "Имя A-Z", + "Name Z-A": "Имя Z-A", + "{{count}} selected_one": "{{count}} выбранный", + "{{count}} selected_few": "{{count}} выбранных", + "{{count}} selected_many": "{{count}} выбранных", + "{{count}} selected_other": "{{count}} выбранный", + "Delete Selected": "Удалить выбранные", + "Created": "Создано", + "No files found": "Файлы не найдены", + "No files uploaded yet": "Файлы ещё не загружены", + "files": "файлы", + "Page {{current}} of {{total}}": "Страница {{current}} из {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Вы уверены, что хотите удалить {{count}} выбранный файл?", + "Are you sure to delete {{count}} selected file(s)?_few": "Вы уверены, что хотите удалить {{count}} выбранных файла?", + "Are you sure to delete {{count}} selected file(s)?_many": "Вы уверены, что хотите удалить {{count}} выбранных файлов?", + "Are you sure to delete {{count}} selected file(s)?_other": "Вы уверены, что хотите удалить {{count}} выбранный файл?", + "Cloud Storage Usage": "Использование облачного хранилища" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 90f2d64e..aa4918dd 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "{{terms}} ඇතුළත් කරන්න", "No search results found": "සෙවුම් ප්‍රතිඵල නොමැත", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS ආහාරය පූරණය කිරීමට අසමත් විය: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} තුළ සෙවීම" + "Search in {{title}}": "{{title}} තුළ සෙවීම", + "Manage Storage": "ගබඩා කළමනාකරණය කරන්න", + "Failed to load files": "ගොනු උඩුගත කිරීමට නොහැකි විය", + "Deleted {{count}} file(s)_one": "ගොනුව මකා දමා ඇත", + "Deleted {{count}} file(s)_other": "ගොනු මකා දමා ඇත", + "Failed to delete {{count}} file(s)_one": "ගොනුව මකා දැමිය නොහැකි විය", + "Failed to delete {{count}} file(s)_other": "ගොනු මකා දැමිය නොහැකි විය", + "Failed to delete files": "ගොනු මකා දැමිය නොහැකි විය", + "Total Files": "මුළු ගොනු", + "Total Size": "මුළු ප්‍රමාණය", + "Quota": "කොටස", + "Used": "භාවිතා කරන ලදී", + "Files": "ගොනු", + "Search files...": "ගොනු සොයන්න...", + "Newest First": "නවතම පළමුව", + "Oldest First": "පැරණිම පළමුව", + "Largest First": "විශාලතම පළමුව", + "Smallest First": "කුඩාතම පළමුව", + "Name A-Z": "නම A-Z", + "Name Z-A": "නම Z-A", + "{{count}} selected_one": "තෝරාගත් ගොනුව", + "{{count}} selected_other": "තෝරාගත් ගොනු", + "Delete Selected": "තෝරාගත් මකා දමන්න", + "Created": "තනන ලදී", + "No files found": "ගොනු හමු නොවීය", + "No files uploaded yet": "ගොනු තවම උඩුගත කර නැත", + "files": "ගොනු", + "Page {{current}} of {{total}}": "පිටුව {{current}} / {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "තෝරාගත් ගොනුව මකන්නට විශ්වාසද?", + "Are you sure to delete {{count}} selected file(s)?_other": "තෝරාගත් ගොනු මකන්නට විශ්වාසද?", + "Cloud Storage Usage": "කලාප ගබඩා භාවිතය" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 5a7b053e..29de8cdc 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "Ange {{terms}}", "No search results found": "Inga sökresultat hittades", "Failed to load OPDS feed: {{status}} {{statusText}}": "Misslyckades med att ladda OPDS-flöde: {{status}} {{statusText}}", - "Search in {{title}}": "Sök i {{title}}" + "Search in {{title}}": "Sök i {{title}}", + "Manage Storage": "Hantera lagring", + "Failed to load files": "Misslyckades att ladda filer", + "Deleted {{count}} file(s)_one": "Raderade filen", + "Deleted {{count}} file(s)_other": "Raderade filerna", + "Failed to delete {{count}} file(s)_one": "Kunde inte radera filen", + "Failed to delete {{count}} file(s)_other": "Kunde inte radera filerna", + "Failed to delete files": "Kunde inte radera filer", + "Total Files": "Totalt antal filer", + "Total Size": "Total storlek", + "Quota": "Kvot", + "Used": "Använd", + "Files": "Filer", + "Search files...": "Sök filer...", + "Newest First": "Nyast först", + "Oldest First": "Äldst först", + "Largest First": "Störst först", + "Smallest First": "Minskst först", + "Name A-Z": "Namn A-Ö", + "Name Z-A": "Namn Ö-A", + "{{count}} selected_one": "Vald fil", + "{{count}} selected_other": "Valda filer", + "Delete Selected": "Radera valda", + "Created": "Skapad", + "No files found": "Inga filer hittades", + "No files uploaded yet": "Inga filer har laddats upp än", + "files": "filer", + "Page {{current}} of {{total}}": "Sida {{current}} av {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Är du säker på att du vill radera filen?", + "Are you sure to delete {{count}} selected file(s)?_other": "Är du säker på att du vill radera filerna?", + "Cloud Storage Usage": "Användning av molnlagring" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 87ad8f5a..c95ff6ca 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "{{terms}} உள்ளிடவும்", "No search results found": "தேடல் முடிவுகள் இல்லை", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS ஊட்டத்தை ஏற்ற முடியவில்லை: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} இல் தேடவும்" + "Search in {{title}}": "{{title}} இல் தேடவும்", + "Manage Storage": "சேமிப்பை நிர்வகி", + "Failed to load files": "கோப்புகளை ஏற்ற முடியவில்லை", + "Deleted {{count}} file(s)_one": "கோப்பு நீக்கப்பட்டது", + "Deleted {{count}} file(s)_other": "கோப்புகள் நீக்கப்பட்டன", + "Failed to delete {{count}} file(s)_one": "கோப்பை நீக்க முடியவில்லை", + "Failed to delete {{count}} file(s)_other": "கோப்புகளை நீக்க முடியவில்லை", + "Failed to delete files": "கோப்புகளை நீக்க முடியவில்லை", + "Total Files": "மொத்த கோப்புகள்", + "Total Size": "மொத்த அளவு", + "Quota": "கோட்டா", + "Used": "பயன்படுத்தப்பட்டது", + "Files": "கோப்புகள்", + "Search files...": "கோப்புகளைத் தேடு...", + "Newest First": "புதியவை முதலில்", + "Oldest First": "பழையவை முதலில்", + "Largest First": "பெரியவை முதலில்", + "Smallest First": "சிறியவை முதலில்", + "Name A-Z": "பெயர் A-ஆல்", + "Name Z-A": "பெயர் ஆல்-A", + "{{count}} selected_one": "தேர்ந்தெடுக்கப்பட்ட கோப்பு", + "{{count}} selected_other": "தேர்ந்தெடுக்கப்பட்ட கோப்புகள்", + "Delete Selected": "தேர்ந்தெடுத்தவை நீக்கு", + "Created": "உருவாக்கப்பட்டது", + "No files found": "கோப்புகள் இல்லை", + "No files uploaded yet": "இன்னும் எந்த கோப்பும் பதிவேற்றப்படவில்லை", + "files": "கோப்புகள்", + "Page {{current}} of {{total}}": "பக்கம் {{current}} / {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "இந்த கோப்பை நீக்க விரும்புகிறீர்களா?", + "Are you sure to delete {{count}} selected file(s)?_other": "இந்த கோப்புகளை நீக்க விரும்புகிறீர்களா?", + "Cloud Storage Usage": "மேக சேமிப்பு பயன்பாடு" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 83e5c8f5..cddd9443 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "ป้อน {{terms}}", "No search results found": "ไม่พบผลการค้นหา", "Failed to load OPDS feed: {{status}} {{statusText}}": "ไม่สามารถโหลดฟีด OPDS ได้: {{status}} {{statusText}}", - "Search in {{title}}": "ค้นหาใน {{title}}" + "Search in {{title}}": "ค้นหาใน {{title}}", + "Manage Storage": "จัดการพื้นที่เก็บข้อมูล", + "Failed to load files": "ไม่สามารถโหลดไฟล์ได้", + "Deleted {{count}} file(s)_other": "ลบไฟล์เรียบร้อยแล้ว", + "Failed to delete {{count}} file(s)_other": "ลบไฟล์ไม่สำเร็จ", + "Failed to delete files": "ลบไฟล์ไม่สำเร็จ", + "Total Files": "จำนวนไฟล์ทั้งหมด", + "Total Size": "ขนาดรวมทั้งหมด", + "Quota": "โควต้า", + "Used": "ใช้ไปแล้ว", + "Files": "ไฟล์", + "Search files...": "ค้นหาไฟล์...", + "Newest First": "ใหม่ที่สุดก่อน", + "Oldest First": "เก่าที่สุดก่อน", + "Largest First": "ใหญ่ที่สุดก่อน", + "Smallest First": "เล็กที่สุดก่อน", + "Name A-Z": "ชื่อ A-ฮ", + "Name Z-A": "ชื่อ ฮ-A", + "{{count}} selected_other": "เลือกไฟล์แล้ว", + "Delete Selected": "ลบไฟล์ที่เลือก", + "Created": "สร้างเมื่อ", + "No files found": "ไม่พบไฟล์", + "No files uploaded yet": "ยังไม่มีการอัปโหลดไฟล์", + "files": "ไฟล์", + "Page {{current}} of {{total}}": "หน้า {{current}} จาก {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "คุณแน่ใจหรือว่าต้องการลบไฟล์ที่เลือก?", + "Cloud Storage Usage": "การใช้งานพื้นที่เก็บข้อมูลคลาวด์" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 70039dab..b39eca77 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -729,5 +729,35 @@ "Enter {{terms}}": "{{terms}} girin", "No search results found": "Arama sonucu bulunamadı", "Failed to load OPDS feed: {{status}} {{statusText}}": "OPDS beslemesi yüklenemedi: {{status}} {{statusText}}", - "Search in {{title}}": "{{title}} içinde ara" + "Search in {{title}}": "{{title}} içinde ara", + "Manage Storage": "Depolamayı Yönet", + "Failed to load files": "Dosyalar yüklenemedi", + "Deleted {{count}} file(s)_one": "{{count}} dosya silindi", + "Deleted {{count}} file(s)_other": "{{count}} dosya silindi", + "Failed to delete {{count}} file(s)_one": "{{count}} dosya silinemedi", + "Failed to delete {{count}} file(s)_other": "{{count}} dosya silinemedi", + "Failed to delete files": "Dosyalar silinemedi", + "Total Files": "Toplam Dosya", + "Total Size": "Toplam Boyut", + "Quota": "Kota", + "Used": "Kullanıldı", + "Files": "Dosyalar", + "Search files...": "Dosyaları ara...", + "Newest First": "En Yeni Önce", + "Oldest First": "En Eski Önce", + "Largest First": "En Büyük Önce", + "Smallest First": "En Küçük Önce", + "Name A-Z": "İsim A-Z", + "Name Z-A": "İsim Z-A", + "{{count}} selected_one": "{{count}} seçili dosya", + "{{count}} selected_other": "{{count}} seçili dosya", + "Delete Selected": "Seçilenleri Sil", + "Created": "Oluşturulma Tarihi", + "No files found": "Dosya bulunamadı", + "No files uploaded yet": "Henüz dosya yüklenmedi", + "files": "dosyalar", + "Page {{current}} of {{total}}": "{{total}} sayfa içinde {{current}}. sayfa", + "Are you sure to delete {{count}} selected file(s)?_one": "Seçilen {{count}} dosyayı silmek istediğinizden emin misiniz?", + "Are you sure to delete {{count}} selected file(s)?_other": "Seçilen {{count}} dosyayı silmek istediğinizden emin misiniz?", + "Cloud Storage Usage": "Bulut Depolama Kullanımı" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index a7099cea..25d70753 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -739,5 +739,43 @@ "Enter {{terms}}": "Введіть {{terms}}", "No search results found": "Результатів пошуку не знайдено", "Failed to load OPDS feed: {{status}} {{statusText}}": "Не вдалося завантажити стрічку OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Пошук у {{title}}" + "Search in {{title}}": "Пошук у {{title}}", + "Manage Storage": "Керування сховищем", + "Failed to load files": "Не вдалося завантажити файли", + "Deleted {{count}} file(s)_one": "Видалено {{count}} файл", + "Deleted {{count}} file(s)_few": "Видалено {{count}} файли", + "Deleted {{count}} file(s)_many": "Видалено {{count}} файлів", + "Deleted {{count}} file(s)_other": "Видалено {{count}} файлів", + "Failed to delete {{count}} file(s)_one": "Не вдалося видалити {{count}} файл", + "Failed to delete {{count}} file(s)_few": "Не вдалося видалити {{count}} файли", + "Failed to delete {{count}} file(s)_many": "Не вдалося видалити {{count}} файлів", + "Failed to delete {{count}} file(s)_other": "Не вдалося видалити {{count}} файлів", + "Failed to delete files": "Не вдалося видалити файли", + "Total Files": "Всього файлів", + "Total Size": "Загальний розмір", + "Quota": "Квота", + "Used": "Використано", + "Files": "Файли", + "Search files...": "Пошук файлів...", + "Newest First": "Спершу новіші", + "Oldest First": "Спершу старіші", + "Largest First": "Спершу найбільші", + "Smallest First": "Спершу найменші", + "Name A-Z": "Ім'я A-Z", + "Name Z-A": "Ім'я Z-A", + "{{count}} selected_one": "Вибрано {{count}} файл", + "{{count}} selected_few": "Вибрано {{count}} файли", + "{{count}} selected_many": "Вибрано {{count}} файлів", + "{{count}} selected_other": "Вибрано {{count}} файлів", + "Delete Selected": "Видалити вибране", + "Created": "Створено", + "No files found": "Файли не знайдено", + "No files uploaded yet": "Файли ще не завантажені", + "files": "файли", + "Page {{current}} of {{total}}": "Сторінка {{current}} з {{total}}", + "Are you sure to delete {{count}} selected file(s)?_one": "Ви впевнені, що хочете видалити {{count}} файл?", + "Are you sure to delete {{count}} selected file(s)?_few": "Ви впевнені, що хочете видалити {{count}} файли?", + "Are you sure to delete {{count}} selected file(s)?_many": "Ви впевнені, що хочете видалити {{count}} файлів?", + "Are you sure to delete {{count}} selected file(s)?_other": "Ви впевнені, що хочете видалити {{count}} файлів?", + "Cloud Storage Usage": "Використання хмарного сховища" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index e10ab42c..c1f10c81 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "Nhập {{terms}}", "No search results found": "Không tìm thấy kết quả tìm kiếm", "Failed to load OPDS feed: {{status}} {{statusText}}": "Không tải được nguồn cấp OPDS: {{status}} {{statusText}}", - "Search in {{title}}": "Tìm kiếm trong {{title}}" + "Search in {{title}}": "Tìm kiếm trong {{title}}", + "Manage Storage": "Quản lý bộ nhớ", + "Failed to load files": "Không tải được tệp", + "Deleted {{count}} file(s)_other": "Đã xóa {{count}} tệp", + "Failed to delete {{count}} file(s)_other": "Không xóa được {{count}} tệp", + "Failed to delete files": "Không xóa được tệp", + "Total Files": "Tổng số tệp", + "Total Size": "Tổng dung lượng", + "Quota": "Dung lượng cho phép", + "Used": "Đã sử dụng", + "Files": "Tệp", + "Search files...": "Tìm tệp...", + "Newest First": "Mới nhất trước", + "Oldest First": "Cũ nhất trước", + "Largest First": "Lớn nhất trước", + "Smallest First": "Nhỏ nhất trước", + "Name A-Z": "Tên A-Z", + "Name Z-A": "Tên Z-A", + "{{count}} selected_other": "Đã chọn {{count}} tệp", + "Delete Selected": "Xóa các mục đã chọn", + "Created": "Đã tạo", + "No files found": "Không tìm thấy tệp", + "No files uploaded yet": "Chưa tải lên tệp nào", + "files": "tệp", + "Page {{current}} of {{total}}": "Trang {{current}} trên {{total}}", + "Are you sure to delete {{count}} selected file(s)?_other": "Bạn có chắc muốn xóa {{count}} tệp đã chọn không?", + "Cloud Storage Usage": "Sử dụng lưu trữ đám mây" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index ea5ada5b..566ea459 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "输入 {{terms}}", "No search results found": "未找到搜索结果", "Failed to load OPDS feed: {{status}} {{statusText}}": "加载 OPDS 源失败:{{status}} {{statusText}}", - "Search in {{title}}": "在 {{title}} 中搜索" + "Search in {{title}}": "在 {{title}} 中搜索", + "Manage Storage": "管理存储", + "Failed to load files": "加载文件失败", + "Deleted {{count}} file(s)_other": "已删除 {{count}} 个文件", + "Failed to delete {{count}} file(s)_other": "删除 {{count}} 个文件失败", + "Failed to delete files": "删除文件失败", + "Total Files": "文件总数", + "Total Size": "总大小", + "Quota": "配额", + "Used": "已使用", + "Files": "文件", + "Search files...": "搜索文件...", + "Newest First": "最新优先", + "Oldest First": "最旧优先", + "Largest First": "最大优先", + "Smallest First": "最小优先", + "Name A-Z": "名称 A-Z", + "Name Z-A": "名称 Z-A", + "{{count}} selected_other": "已选择 {{count}} 个文件", + "Delete Selected": "删除选中项", + "Created": "创建时间", + "No files found": "未找到文件", + "No files uploaded yet": "尚未上传文件", + "files": "文件", + "Page {{current}} of {{total}}": "第 {{current}} 页,共 {{total}} 页", + "Are you sure to delete {{count}} selected file(s)?_other": "确定要删除已选择的 {{count}} 个文件吗?", + "Cloud Storage Usage": "云存储使用情况" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index b2974dc8..a3037910 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -724,5 +724,31 @@ "Enter {{terms}}": "輸入 {{terms}}", "No search results found": "未找到搜尋結果", "Failed to load OPDS feed: {{status}} {{statusText}}": "載入 OPDS 資料源失敗:{{status}} {{statusText}}", - "Search in {{title}}": "在 {{title}} 中搜尋" + "Search in {{title}}": "在 {{title}} 中搜尋", + "Manage Storage": "管理儲存", + "Failed to load files": "載入檔案失敗", + "Deleted {{count}} file(s)_other": "已刪除 {{count}} 個檔案", + "Failed to delete {{count}} file(s)_other": "刪除 {{count}} 個檔案失敗", + "Failed to delete files": "刪除檔案失敗", + "Total Files": "檔案總數", + "Total Size": "總大小", + "Quota": "配額", + "Used": "已使用", + "Files": "檔案", + "Search files...": "搜尋檔案...", + "Newest First": "最新優先", + "Oldest First": "最舊優先", + "Largest First": "最大優先", + "Smallest First": "最小優先", + "Name A-Z": "名稱 A-Z", + "Name Z-A": "名稱 Z-A", + "{{count}} selected_other": "已選擇 {{count}} 個檔案", + "Delete Selected": "刪除所選", + "Created": "建立時間", + "No files found": "找不到檔案", + "No files uploaded yet": "尚未上傳檔案", + "files": "檔案", + "Page {{current}} of {{total}}": "第 {{current}} 頁,共 {{total}} 頁", + "Are you sure to delete {{count}} selected file(s)?_other": "確定要刪除已選擇的 {{count}} 個檔案嗎?", + "Cloud Storage Usage": "雲端儲存使用情況" } diff --git a/apps/readest-app/src/app/user/components/AccountActions.tsx b/apps/readest-app/src/app/user/components/AccountActions.tsx index 05e29028..8fbc5c44 100644 --- a/apps/readest-app/src/app/user/components/AccountActions.tsx +++ b/apps/readest-app/src/app/user/components/AccountActions.tsx @@ -53,6 +53,7 @@ interface AccountActionsProps { onConfirmDelete: () => void; onRestorePurchase?: () => void; onManageSubscription?: () => void; + onManageStorage?: () => void; } const AccountActions: React.FC = ({ @@ -63,6 +64,7 @@ const AccountActions: React.FC = ({ onConfirmDelete, onRestorePurchase, onManageSubscription, + onManageStorage, }) => { const _ = useTranslation(); const { appService } = useEnv(); @@ -104,6 +106,14 @@ const AccountActions: React.FC = ({ ) )} + {onManageStorage && ( + + )} + + + {loading && } + + {/* Files List - Grouped by Book */} +
+ + + + + + + + + + + {!filesLoaded ? ( + <> + {[...Array(5)].map((_, i) => ( + + + + + + + ))} + + ) : groupedFiles.size === 0 ? ( + + + + ) : ( + Array.from(groupedFiles.entries()).map(([bookHash, bookFiles]) => { + const mainFile = getMainBookFile(bookFiles); + const isExpanded = expandedBooks.has(bookHash); + const hasMultipleFiles = bookFiles.length > 1; + const bookSelected = isBookSelected(bookFiles); + const bookPartiallySelected = isBookPartiallySelected(bookFiles); + + if (!mainFile) return null; + + return ( + + {/* Main book row */} + + + + + + + + {/* Expanded files (excluding covers unless expanded) */} + {isExpanded && + bookFiles.map((file) => ( + + + + + + + ))} + + ); + }) + )} + +
+
+ handleSelectAll(e.target.checked)} + className='checkbox checkbox-sm' + disabled={!filesLoaded || loading} + /> +
+
{_('File Name')}{_('Size')}{_('Created')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {searchQuery ? _('No files found') : _('No files uploaded yet')} +
+
+
+ { + if (el) el.indeterminate = bookPartiallySelected; + }} + onChange={(e) => handleSelectBook(bookFiles, e.target.checked)} + disabled={loading} + className='checkbox checkbox-sm' + /> + {hasMultipleFiles && ( + + )} +
+
+
+
+ + {getFileName(mainFile.file_key)} + + {hasMultipleFiles && ( + + ({bookFiles.length} {_('files')}) + + )} +
+ + {formatFileSize(getBookTotalSize(bookFiles))} ·{' '} + {formatDate(mainFile.created_at)} + +
+
+ {formatFileSize(getBookTotalSize(bookFiles))} + + {formatDate(mainFile.created_at)} +
+
+ + handleSelectFile(file.file_key, e.target.checked) + } + disabled={loading} + className='checkbox checkbox-sm' + /> +
+
+
+ + {getFileName(file.file_key)} + +
+
+ {formatFileSize(file.file_size)} + {formatDate(file.created_at)}
+
+ + {/* Pagination */} +
+ + + {_('Page {{current}} of {{total}}', { current: currentPage, total: totalPages })} + + +
+ + + {/* Confirm Delete Modal */} + {showConfirmDelete && ( +
+ { + setShowConfirmDelete(false); + }} + onConfirm={() => { + handleDeleteSelected(); + setShowConfirmDelete(false); + }} + /> +
+ )} + + ); +}; + +export default StorageManager; diff --git a/apps/readest-app/src/app/user/page.tsx b/apps/readest-app/src/app/user/page.tsx index e79c536b..4f9e3547 100644 --- a/apps/readest-app/src/app/user/page.tsx +++ b/apps/readest-app/src/app/user/page.tsx @@ -22,6 +22,7 @@ import { restoreIAPPurchases, getSubscriptionSuccessUrl as getIAPSubscriptionSuccessUrl, } from '@/libs/payment/iap/client'; +import { isPurchaseProduct } from '@/libs/payment/iap/utils'; import { createStripeCheckoutSession, redirectToStripeCheckout, @@ -38,8 +39,8 @@ import UserInfo from './components/UserInfo'; import UsageStats from './components/UsageStats'; import PlansComparison from './components/PlansComparison'; import AccountActions from './components/AccountActions'; +import StorageManager from './components/StorageManager'; import Checkout from './components/Checkout'; -import { isPurchaseProduct } from '@/libs/payment/iap/utils'; type CheckoutState = { clientSecret: string; @@ -56,6 +57,7 @@ const ProfilePage = () => { const [loading, setLoading] = useState(false); const [showEmbeddedCheckout, setShowEmbeddedCheckout] = useState(false); + const [showStorageManager, setShowStorageManager] = useState(false); const [checkoutState, setCheckoutState] = useState({ clientSecret: '', sessionId: '', @@ -87,6 +89,8 @@ const ProfilePage = () => { const handleGoBack = () => { if (showEmbeddedCheckout) { setShowEmbeddedCheckout(false); + } else if (showStorageManager) { + setShowStorageManager(false); } else { navigateToLibrary(router); } @@ -200,6 +204,10 @@ const ProfilePage = () => { handleConfirmDelete(_('Failed to delete user. Please try again later.')); }; + const handleManageStorage = () => { + setShowStorageManager(true); + }; + if (!mounted) { return null; } @@ -262,28 +270,37 @@ const ProfilePage = () => { planDetails={userPlanDetails} /> - + {!showStorageManager && } -
- -
+ {showStorageManager ? ( +
+ +
+ ) : ( + <> +
+ +
+
+ +
+ + )} -
- -
diff --git a/apps/readest-app/src/libs/storage.ts b/apps/readest-app/src/libs/storage.ts index f8c7b662..a1ae07fc 100644 --- a/apps/readest-app/src/libs/storage.ts +++ b/apps/readest-app/src/libs/storage.ts @@ -15,6 +15,9 @@ const API_ENDPOINTS = { upload: getAPIBaseUrl() + '/storage/upload', download: getAPIBaseUrl() + '/storage/download', delete: getAPIBaseUrl() + '/storage/delete', + stats: getAPIBaseUrl() + '/storage/stats', + list: getAPIBaseUrl() + '/storage/list', + purge: getAPIBaseUrl() + '/storage/purge', }; export const createProgressHandler = ( @@ -171,3 +174,119 @@ export const deleteFile = async (filePath: string) => { throw new Error('File deletion failed'); } }; + +export interface StorageStats { + totalFiles: number; + totalSize: number; + usage: number; + quota: number; + usagePercentage: number; + byBookHash: Array<{ + bookHash: string | null; + fileCount: number; + totalSize: number; + }>; +} + +export const getStorageStats = async (): Promise => { + try { + const response = await fetchWithAuth(API_ENDPOINTS.stats, { + method: 'GET', + }); + + return await response.json(); + } catch (error) { + console.error('Get storage stats failed:', error); + throw new Error('Get storage stats failed'); + } +}; + +export interface FileRecord { + file_key: string; + file_size: number; + book_hash: string | null; + created_at: string; + updated_at: string | null; +} + +export interface ListFilesParams { + page?: number; + pageSize?: number; + sortBy?: 'created_at' | 'updated_at' | 'file_size' | 'file_key'; + sortOrder?: 'asc' | 'desc'; + bookHash?: string; + search?: string; +} + +interface ListFilesResponse { + files: FileRecord[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +export const listFiles = async (params?: ListFilesParams): Promise => { + try { + const queryParams = new URLSearchParams(); + + if (params?.page) queryParams.set('page', params.page.toString()); + if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString()); + if (params?.sortBy) queryParams.set('sortBy', params.sortBy); + if (params?.sortOrder) queryParams.set('sortOrder', params.sortOrder); + if (params?.bookHash) queryParams.set('bookHash', params.bookHash); + if (params?.search) queryParams.set('search', params.search); + + const url = queryParams.toString() + ? `${API_ENDPOINTS.list}?${queryParams.toString()}` + : API_ENDPOINTS.list; + + const response = await fetchWithAuth(url, { + method: 'GET', + }); + + return await response.json(); + } catch (error) { + console.error('List files failed:', error); + throw new Error('List files failed'); + } +}; + +interface PurgeFilesResult { + success: string[]; + failed: Array<{ fileKey: string; error: string }>; + deletedCount: number; + failedCount: number; +} + +export const purgeFiles = async ( + filePathsOrKeys: string[], + isFileKeys = false, +): Promise => { + try { + let fileKeys: string[]; + + if (isFileKeys) { + fileKeys = filePathsOrKeys; + } else { + const userId = await getUserID(); + if (!userId) { + throw new Error('Not authenticated'); + } + fileKeys = filePathsOrKeys.map((path) => `${userId}/${path}`); + } + + const response = await fetchWithAuth(API_ENDPOINTS.purge, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fileKeys }), + }); + + return await response.json(); + } catch (error) { + console.error('Purge files failed:', error); + throw new Error('Purge files failed'); + } +}; diff --git a/apps/readest-app/src/pages/api/storage/list.ts b/apps/readest-app/src/pages/api/storage/list.ts new file mode 100644 index 00000000..36ca3c9a --- /dev/null +++ b/apps/readest-app/src/pages/api/storage/list.ts @@ -0,0 +1,124 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createSupabaseAdminClient } from '@/utils/supabase'; +import { corsAllMethods, runMiddleware } from '@/utils/cors'; +import { validateUserAndToken } from '@/utils/access'; + +interface FileRecord { + file_key: string; + file_size: number; + book_hash: string | null; + created_at: string; + updated_at: string | null; +} + +interface ListFilesResponse { + files: FileRecord[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await runMiddleware(req, res, corsAllMethods); + + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { user, token } = await validateUserAndToken(req.headers['authorization']); + if (!user || !token) { + return res.status(403).json({ error: 'Not authenticated' }); + } + + const reqQuery = req.query as { + page?: string; + pageSize?: string; + sortBy?: string; + sortOrder?: string; + bookHash?: string; + search?: string; + }; + const page = parseInt(reqQuery.page as string) || 1; + const pageSize = Math.min(parseInt(reqQuery.pageSize as string) || 50, 100); + const sortBy = (reqQuery.sortBy as string) || 'created_at'; + const sortOrder = (reqQuery.sortOrder as string) === 'asc' ? 'asc' : 'desc'; + const bookHash = reqQuery.bookHash as string | undefined; + const search = reqQuery.search as string | undefined; + + const supabase = createSupabaseAdminClient(); + + let query = supabase + .from('files') + .select('file_key, file_size, book_hash, created_at, updated_at', { count: 'exact' }) + .eq('user_id', user.id) + .is('deleted_at', null); + + if (bookHash) { + query = query.eq('book_hash', bookHash); + } + + if (search) { + query = query.ilike('file_key', `%${search}%`); + } + + const validSortColumns = ['created_at', 'updated_at', 'file_size', 'file_key']; + const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'created_at'; + query = query.order(sortColumn, { ascending: sortOrder === 'asc' }); + + const from = (page - 1) * pageSize; + const to = from + pageSize - 1; + query = query.range(from, to); + + const { data: files, error: filesError, count } = await query; + + if (filesError) { + console.error('Error querying files:', filesError); + return res.status(500).json({ error: 'Failed to retrieve files' }); + } + + const total = count || 0; + const totalPages = Math.ceil(total / pageSize); + + // Get all book_hashes from the paginated results + const bookHashes = Array.from( + new Set((files || []).map((f) => f.book_hash).filter((hash): hash is string => !!hash)), + ); + + // Fetch all files with the same book_hashes to ensure complete book groups + // IMPORTANT: We don't apply the search filter here. This ensures that ALL files + // for matched books are included (e.g., cover.png files), even if they don't + // match the search term. This is crucial for proper book grouping and selection. + let allRelatedFiles = files || []; + if (bookHashes.length > 0) { + const relatedQuery = supabase + .from('files') + .select('file_key, file_size, book_hash, created_at, updated_at') + .eq('user_id', user.id) + .is('deleted_at', null) + .in('book_hash', bookHashes); + + const { data: relatedFiles, error: relatedError } = await relatedQuery; + + if (!relatedError && relatedFiles) { + const fileMap = new Map(allRelatedFiles.map((f) => [f.file_key, f])); + relatedFiles.forEach((f) => fileMap.set(f.file_key, f)); + allRelatedFiles = Array.from(fileMap.values()); + } + } + + const response: ListFilesResponse = { + files: allRelatedFiles, + total, + page, + pageSize, + totalPages, + }; + + return res.status(200).json(response); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Something went wrong' }); + } +} diff --git a/apps/readest-app/src/pages/api/storage/purge.ts b/apps/readest-app/src/pages/api/storage/purge.ts new file mode 100644 index 00000000..bfe3eeb0 --- /dev/null +++ b/apps/readest-app/src/pages/api/storage/purge.ts @@ -0,0 +1,146 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { corsAllMethods, runMiddleware } from '@/utils/cors'; +import { createSupabaseAdminClient } from '@/utils/supabase'; +import { validateUserAndToken } from '@/utils/access'; +import { deleteObject } from '@/utils/object'; + +interface BulkDeleteResult { + success: string[]; + failed: Array<{ fileKey: string; error: string }>; + deletedCount: number; + failedCount: number; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await runMiddleware(req, res, corsAllMethods); + + if (req.method !== 'DELETE') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { user, token } = await validateUserAndToken(req.headers['authorization']); + if (!user || !token) { + return res.status(403).json({ error: 'Not authenticated' }); + } + + const { fileKeys } = req.body; + + if (!fileKeys || !Array.isArray(fileKeys)) { + return res.status(400).json({ error: 'Missing or invalid fileKeys array' }); + } + + if (fileKeys.length === 0) { + return res.status(400).json({ error: 'fileKeys array cannot be empty' }); + } + + if (fileKeys.length > 100) { + return res.status(400).json({ error: 'Cannot delete more than 100 files at once' }); + } + + if (!fileKeys.every((key) => typeof key === 'string')) { + return res.status(400).json({ error: 'All fileKeys must be strings' }); + } + + const supabase = createSupabaseAdminClient(); + + // Fetch all files that match the provided keys and belong to the user + const { data: fileRecords, error: fileError } = await supabase + .from('files') + .select('id, user_id, file_key') + .eq('user_id', user.id) + .in('file_key', fileKeys) + .is('deleted_at', null); + + if (fileError) { + console.error('Error querying files:', fileError); + return res.status(500).json({ error: 'Failed to retrieve files for deletion' }); + } + + if (!fileRecords || fileRecords.length === 0) { + return res.status(404).json({ error: 'No matching files found' }); + } + + // Verify all files belong to the user + const unauthorizedFiles = fileRecords.filter((record) => record.user_id !== user.id); + if (unauthorizedFiles.length > 0) { + return res.status(403).json({ error: 'Unauthorized access to one or more files' }); + } + + // Process deletions + const results = await Promise.allSettled( + fileRecords.map(async (fileRecord) => { + try { + // Delete from storage + await deleteObject(fileRecord.file_key); + + // Delete from database + const { error: deleteError } = await supabase + .from('files') + .delete() + .eq('id', fileRecord.id); + + if (deleteError) { + throw new Error(`Database deletion failed: ${deleteError.message}`); + } + + return { fileKey: fileRecord.file_key, success: true }; + } catch (error) { + console.error(`Error deleting file ${fileRecord.file_key}:`, error); + return { + fileKey: fileRecord.file_key, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }), + ); + + const success: string[] = []; + const failed: Array<{ fileKey: string; error: string }> = []; + + results.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value.success) { + success.push(result.value.fileKey); + } else { + failed.push({ + fileKey: result.value.fileKey, + error: result.value.error || 'Unknown error', + }); + } + } else { + failed.push({ + fileKey: 'unknown', + error: result.reason?.message || 'Promise rejected', + }); + } + }); + + // Handle files that weren't found in the database + const foundFileKeys = new Set(fileRecords.map((record) => record.file_key)); + const notFoundKeys = fileKeys.filter((key) => !foundFileKeys.has(key)); + notFoundKeys.forEach((key) => { + failed.push({ + fileKey: key, + error: 'File not found or already deleted', + }); + }); + + const response: BulkDeleteResult = { + success, + failed, + deletedCount: success.length, + failedCount: failed.length, + }; + + // Return 207 Multi-Status if there are partial failures + const statusCode = + failed.length > 0 && success.length > 0 ? 207 : failed.length > 0 ? 500 : 200; + + return res.status(statusCode).json(response); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Something went wrong' }); + } +} diff --git a/apps/readest-app/src/pages/api/storage/stats.ts b/apps/readest-app/src/pages/api/storage/stats.ts new file mode 100644 index 00000000..d84509b9 --- /dev/null +++ b/apps/readest-app/src/pages/api/storage/stats.ts @@ -0,0 +1,109 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createSupabaseAdminClient } from '@/utils/supabase'; +import { corsAllMethods, runMiddleware } from '@/utils/cors'; +import { validateUserAndToken, getStoragePlanData } from '@/utils/access'; + +interface StorageStats { + totalFiles: number; + totalSize: number; + usage: number; + quota: number; + usagePercentage: number; + byBookHash: Array<{ + bookHash: string | null; + fileCount: number; + totalSize: number; + }>; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await runMiddleware(req, res, corsAllMethods); + + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { user, token } = await validateUserAndToken(req.headers['authorization']); + if (!user || !token) { + return res.status(403).json({ error: 'Not authenticated' }); + } + + const supabase = createSupabaseAdminClient(); + + // Get total file count and size + const { data: totalStats, error: totalError } = await supabase + .from('files') + .select('file_size') + .eq('user_id', user.id) + .is('deleted_at', null); + + if (totalError) { + console.error('Error querying total stats:', totalError); + return res.status(500).json({ error: 'Failed to retrieve storage statistics' }); + } + + const totalFiles = totalStats?.length || 0; + const totalSize = totalStats?.reduce((sum, file) => sum + (file.file_size || 0), 0) || 0; + + // Get storage plan data + const { usage, quota } = getStoragePlanData(token); + const usagePercentage = quota > 0 ? Math.round((usage / quota) * 100) : 0; + + // Get stats grouped by book_hash + const { data: bookHashStats, error: bookHashError } = await supabase.rpc( + 'get_storage_by_book_hash', + { p_user_id: user.id }, + ); + + // Fallback if RPC function doesn't exist - manual aggregation + let byBookHash: Array<{ bookHash: string | null; fileCount: number; totalSize: number }> = []; + + if (bookHashError) { + console.warn('RPC function not available, using fallback aggregation:', bookHashError); + + const { data: allFiles, error: filesError } = await supabase + .from('files') + .select('book_hash, file_size') + .eq('user_id', user.id) + .is('deleted_at', null); + + if (!filesError && allFiles) { + const grouped = new Map(); + + allFiles.forEach((file) => { + const key = file.book_hash; + const current = grouped.get(key) || { count: 0, size: 0 }; + grouped.set(key, { + count: current.count + 1, + size: current.size + file.file_size, + }); + }); + + byBookHash = Array.from(grouped.entries()) + .map(([bookHash, stats]) => ({ + bookHash, + fileCount: stats.count, + totalSize: stats.size, + })) + .sort((a, b) => b.totalSize - a.totalSize); + } + } else if (bookHashStats) { + byBookHash = bookHashStats; + } + + const response: StorageStats = { + totalFiles, + totalSize, + usage, + quota, + usagePercentage, + byBookHash, + }; + + return res.status(200).json(response); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Something went wrong' }); + } +}