feat: support cloud storage management (#2636)
This commit is contained in:
@@ -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": "استخدام التخزين السحابي"
|
||||
}
|
||||
|
||||
@@ -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": "ক্লাউড স্টোরেজ ব্যবহৃত"
|
||||
}
|
||||
|
||||
@@ -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": "སྤྲིན་གནས་སྣོད་གསོག་ལུས་སྐོར།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "استفاده از فضای ذخیرهسازی ابری"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "क्लाउड स्टोरेज उपयोग"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "クラウドストレージ使用量"
|
||||
}
|
||||
|
||||
@@ -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": "클라우드 저장소 사용량"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Использование облачного хранилища"
|
||||
}
|
||||
|
||||
@@ -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": "කලාප ගබඩා භාවිතය"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "மேக சேமிப்பு பயன்பாடு"
|
||||
}
|
||||
|
||||
@@ -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": "การใช้งานพื้นที่เก็บข้อมูลคลาวด์"
|
||||
}
|
||||
|
||||
@@ -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ı"
|
||||
}
|
||||
|
||||
@@ -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": "Використання хмарного сховища"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "云存储使用情况"
|
||||
}
|
||||
|
||||
@@ -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": "雲端儲存使用情況"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ interface AccountActionsProps {
|
||||
onConfirmDelete: () => void;
|
||||
onRestorePurchase?: () => void;
|
||||
onManageSubscription?: () => void;
|
||||
onManageStorage?: () => void;
|
||||
}
|
||||
|
||||
const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
@@ -63,6 +64,7 @@ const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
onConfirmDelete,
|
||||
onRestorePurchase,
|
||||
onManageSubscription,
|
||||
onManageStorage,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
@@ -104,6 +106,14 @@ const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
{onManageStorage && (
|
||||
<button
|
||||
onClick={onManageStorage}
|
||||
className='w-full rounded-lg bg-purple-100 px-6 py-3 font-medium text-purple-600 transition-colors hover:bg-purple-200 md:w-auto'
|
||||
>
|
||||
{_('Manage Storage')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onResetPassword}
|
||||
className='w-full rounded-lg bg-gray-200 px-6 py-3 font-medium text-gray-800 transition-colors hover:bg-gray-300 md:w-auto'
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibrary } from '@/hooks/useLibrary';
|
||||
import {
|
||||
listFiles,
|
||||
getStorageStats,
|
||||
purgeFiles,
|
||||
type FileRecord,
|
||||
type StorageStats,
|
||||
type ListFilesParams,
|
||||
} from '@/libs/storage';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import Alert from '@/components/Alert';
|
||||
|
||||
const StorageManager = () => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { libraryLoaded } = useLibrary();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
const [files, setFiles] = useState<FileRecord[]>([]);
|
||||
const [stats, setStats] = useState<StorageStats | null>(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [sortBy, setSortBy] = useState<ListFilesParams['sortBy']>('created_at');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
const [expandedBooks, setExpandedBooks] = useState<Set<string>>(new Set());
|
||||
|
||||
const loadFiles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: ListFilesParams = {
|
||||
page: currentPage,
|
||||
pageSize: 20,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
};
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
params.search = searchQuery.trim();
|
||||
}
|
||||
|
||||
const response = await listFiles(params);
|
||||
setFiles(response.files);
|
||||
setTotalPages(response.totalPages);
|
||||
setFilesLoaded(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to load files:', error);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to load files'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, sortBy, sortOrder, searchQuery, _]);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const statsData = await getStorageStats();
|
||||
setStats(statsData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
loadStats();
|
||||
}, [loadFiles, loadStats]);
|
||||
|
||||
// Group files by book_hash
|
||||
const groupedFiles = React.useMemo(() => {
|
||||
const groups = new Map<string, FileRecord[]>();
|
||||
|
||||
files.forEach((file) => {
|
||||
const bookHash = file.book_hash || 'no-book';
|
||||
if (!groups.has(bookHash)) {
|
||||
groups.set(bookHash, []);
|
||||
}
|
||||
groups.get(bookHash)!.push(file);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}, [files]);
|
||||
|
||||
const getFileName = (fileKey: string): string => {
|
||||
const parts = fileKey.split('/');
|
||||
return parts[parts.length - 1] || fileKey;
|
||||
};
|
||||
|
||||
const isCoverFile = (file: FileRecord): boolean => {
|
||||
return getFileName(file.file_key).toLowerCase() === 'cover.png';
|
||||
};
|
||||
|
||||
// Get main book file (first non-cover file)
|
||||
const getMainBookFile = (bookFiles: FileRecord[]): FileRecord | null => {
|
||||
return bookFiles.find((f) => !isCoverFile(f)) || bookFiles[0] || null;
|
||||
};
|
||||
|
||||
// Get all files for a book including covers
|
||||
const getAllBookFiles = (bookFiles: FileRecord[]): FileRecord[] => {
|
||||
return bookFiles;
|
||||
};
|
||||
|
||||
const toggleBookExpansion = (bookHash: string) => {
|
||||
const newExpanded = new Set(expandedBooks);
|
||||
if (newExpanded.has(bookHash)) {
|
||||
newExpanded.delete(bookHash);
|
||||
} else {
|
||||
newExpanded.add(bookHash);
|
||||
}
|
||||
setExpandedBooks(newExpanded);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFiles(new Set(files.map((f) => f.file_key)));
|
||||
} else {
|
||||
setSelectedFiles(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectBook = (bookFiles: FileRecord[], checked: boolean) => {
|
||||
const newSelected = new Set(selectedFiles);
|
||||
const allBookFiles = getAllBookFiles(bookFiles);
|
||||
|
||||
allBookFiles.forEach((file) => {
|
||||
if (checked) {
|
||||
newSelected.add(file.file_key);
|
||||
} else {
|
||||
newSelected.delete(file.file_key);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedFiles(newSelected);
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileKey: string, checked: boolean) => {
|
||||
const newSelected = new Set(selectedFiles);
|
||||
if (checked) {
|
||||
newSelected.add(fileKey);
|
||||
} else {
|
||||
newSelected.delete(fileKey);
|
||||
}
|
||||
setSelectedFiles(newSelected);
|
||||
};
|
||||
|
||||
const isBookSelected = (bookFiles: FileRecord[]): boolean => {
|
||||
const allBookFiles = getAllBookFiles(bookFiles);
|
||||
return allBookFiles.length > 0 && allBookFiles.every((f) => selectedFiles.has(f.file_key));
|
||||
};
|
||||
|
||||
const isBookPartiallySelected = (bookFiles: FileRecord[]): boolean => {
|
||||
const allBookFiles = getAllBookFiles(bookFiles);
|
||||
const selectedCount = allBookFiles.filter((f) => selectedFiles.has(f.file_key)).length;
|
||||
return selectedCount > 0 && selectedCount < allBookFiles.length;
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
if (selectedFiles.size === 0) return;
|
||||
if (!libraryLoaded || !appService) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const fileKeys = Array.from(selectedFiles);
|
||||
const fileRecords = files.filter((f) => selectedFiles.has(f.file_key));
|
||||
const selectedBookHashes = new Set(
|
||||
fileRecords.map((f) => f.book_hash).filter((hash): hash is string => !!hash),
|
||||
);
|
||||
|
||||
const result = await purgeFiles(fileKeys, true);
|
||||
|
||||
const { library, setLibrary } = useLibraryStore.getState();
|
||||
library
|
||||
.filter((book) => selectedBookHashes.has(book.hash))
|
||||
.forEach((book) => {
|
||||
book.uploadedAt = null;
|
||||
book.updatedAt = Date.now();
|
||||
});
|
||||
setLibrary(library);
|
||||
appService.saveLibraryBooks(library);
|
||||
|
||||
if (result.deletedCount > 0) {
|
||||
await loadFiles();
|
||||
await loadStats();
|
||||
setSelectedFiles(new Set());
|
||||
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Deleted {{count}} file(s)', { count: result.deletedCount }),
|
||||
});
|
||||
|
||||
if (result.failedCount > 0) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to delete {{count}} file(s)', { count: result.failedCount }),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete files:', error);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to delete files'),
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowConfirmDelete(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchChange = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
setSearchQuery(value);
|
||||
setCurrentPage(1);
|
||||
}, 1000),
|
||||
[setSearchQuery, setCurrentPage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchChange(searchInput);
|
||||
}, [searchInput, handleSearchChange]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const getBookTotalSize = (bookFiles: FileRecord[]): number => {
|
||||
return bookFiles.reduce((sum, file) => sum + file.file_size, 0);
|
||||
};
|
||||
|
||||
const isAllSelected = files.length > 0 && selectedFiles.size === files.length;
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-6'>
|
||||
{/* Stats Section */}
|
||||
{stats ? (
|
||||
<div className='bg-base-100 border-base-300 rounded-lg border p-4'>
|
||||
<h3 className='text-base-content mb-4 text-lg font-semibold'>
|
||||
{_('Cloud Storage Usage')}
|
||||
</h3>
|
||||
<div className='grid grid-cols-2 gap-4 sm:grid-cols-4'>
|
||||
<div>
|
||||
<div className='text-base-content/60 text-sm'>{_('Total Files')}</div>
|
||||
<div className='text-base-content text-xl font-semibold'>{stats.totalFiles}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-base-content/60 text-sm'>{_('Total Size')}</div>
|
||||
<div className='text-base-content text-xl font-semibold'>
|
||||
{formatFileSize(stats.totalSize)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-base-content/60 text-sm'>{_('Quota')}</div>
|
||||
<div className='text-base-content text-xl font-semibold'>
|
||||
{formatFileSize(stats.quota)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-base-content/60 text-sm'>{_('Used')}</div>
|
||||
<div className='text-base-content text-xl font-semibold'>
|
||||
{stats.usagePercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-base-300 mt-4 h-2 w-full overflow-hidden rounded-full'>
|
||||
<div
|
||||
className='bg-primary h-full transition-all'
|
||||
style={{ width: `${Math.min(stats.usagePercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='bg-base-100 border-base-300 rounded-lg border p-4'>
|
||||
<div className='skeleton mb-4 h-6 w-32'></div>
|
||||
<div className='grid grid-cols-2 gap-4 sm:grid-cols-4'>
|
||||
<div className='skeleton h-16 w-full'></div>
|
||||
<div className='skeleton h-16 w-full'></div>
|
||||
<div className='skeleton h-16 w-full'></div>
|
||||
<div className='skeleton h-16 w-full'></div>
|
||||
</div>
|
||||
<div className='skeleton mt-4 h-2 w-full rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files Section */}
|
||||
<div className='bg-base-100 border-base-300 rounded-lg border'>
|
||||
<div className='border-base-300 flex flex-col gap-4 border-b p-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='hidden items-center justify-center sm:flex'>
|
||||
<h3 className='text-base-content text-lg font-semibold'>{_('Files')}</h3>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={_('Search files...')}
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className='input input-bordered input-sm w-full sm:w-64'
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={`${sortBy}-${sortOrder}`}
|
||||
onChange={(e) => {
|
||||
const [newSortBy, newSortOrder] = e.target.value.split('-');
|
||||
setSortBy(newSortBy as ListFilesParams['sortBy']);
|
||||
setSortOrder(newSortOrder as 'asc' | 'desc');
|
||||
}}
|
||||
disabled={loading}
|
||||
className='select select-bordered select-sm'
|
||||
>
|
||||
<option value='created_at-desc'>{_('Newest First')}</option>
|
||||
<option value='created_at-asc'>{_('Oldest First')}</option>
|
||||
<option value='file_size-desc'>{_('Largest First')}</option>
|
||||
<option value='file_size-asc'>{_('Smallest First')}</option>
|
||||
<option value='file_key-asc'>{_('Name A-Z')}</option>
|
||||
<option value='file_key-desc'>{_('Name Z-A')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Bar */}
|
||||
<div className='bg-base-200 border-base-300 flex items-center justify-between border-b p-4'>
|
||||
<span className='text-base-content text-sm'>
|
||||
{_('{{count}} selected', { count: selectedFiles.size })}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
className='btn btn-error btn-sm'
|
||||
disabled={loading || selectedFiles.size === 0}
|
||||
>
|
||||
{_('Delete Selected')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && <Spinner loading />}
|
||||
|
||||
{/* Files List - Grouped by Book */}
|
||||
<div className='w-full'>
|
||||
<table className='table-sm table w-full [&_td]:px-2 [&_td]:py-1 [&_th]:px-2 [&_th]:py-1'>
|
||||
<thead className='h-10'>
|
||||
<tr>
|
||||
<th className='w-12'>
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={isAllSelected}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
className='checkbox checkbox-sm'
|
||||
disabled={!filesLoaded || loading}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className='!ps-0'>{_('File Name')}</th>
|
||||
<th className='hidden sm:table-cell'>{_('Size')}</th>
|
||||
<th className='hidden sm:table-cell'>{_('Created')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!filesLoaded ? (
|
||||
<>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<tr key={i}>
|
||||
<td className='min-w-16'>
|
||||
<div className='skeleton h-5 w-5'></div>
|
||||
</td>
|
||||
<td className='max-w-0 !ps-0 sm:w-[80%]'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='skeleton h-4 w-3/4'></div>
|
||||
<div className='skeleton h-3 w-1/2 sm:hidden'></div>
|
||||
</div>
|
||||
</td>
|
||||
<td className='hidden sm:table-cell'>
|
||||
<div className='skeleton h-4 w-16'></div>
|
||||
</td>
|
||||
<td className='hidden sm:table-cell'>
|
||||
<div className='skeleton h-4 w-20'></div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : groupedFiles.size === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className='text-center'>
|
||||
<div className='text-base-content/60 py-8'>
|
||||
{searchQuery ? _('No files found') : _('No files uploaded yet')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
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 (
|
||||
<React.Fragment key={bookHash}>
|
||||
{/* Main book row */}
|
||||
<tr className='hover'>
|
||||
<td>
|
||||
<div className='flex items-center gap-1'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={bookSelected}
|
||||
ref={(el) => {
|
||||
if (el) el.indeterminate = bookPartiallySelected;
|
||||
}}
|
||||
onChange={(e) => handleSelectBook(bookFiles, e.target.checked)}
|
||||
disabled={loading}
|
||||
className='checkbox checkbox-sm'
|
||||
/>
|
||||
{hasMultipleFiles && (
|
||||
<button
|
||||
onClick={() => toggleBookExpansion(bookHash)}
|
||||
className='btn btn-ghost btn-xs'
|
||||
>
|
||||
{isExpanded ? '−' : '+'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className='max-w-0 !ps-0 sm:w-[80%]'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-base-content block max-w-full truncate font-medium'>
|
||||
{getFileName(mainFile.file_key)}
|
||||
</span>
|
||||
{hasMultipleFiles && (
|
||||
<span className='text-base-content/60 flex-shrink-0 whitespace-nowrap text-xs'>
|
||||
({bookFiles.length} {_('files')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className='text-base-content/60 text-xs sm:hidden'>
|
||||
{formatFileSize(getBookTotalSize(bookFiles))} ·{' '}
|
||||
{formatDate(mainFile.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className='hidden whitespace-nowrap sm:table-cell'>
|
||||
{formatFileSize(getBookTotalSize(bookFiles))}
|
||||
</td>
|
||||
<td className='hidden whitespace-nowrap sm:table-cell'>
|
||||
{formatDate(mainFile.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Expanded files (excluding covers unless expanded) */}
|
||||
{isExpanded &&
|
||||
bookFiles.map((file) => (
|
||||
<tr key={file.file_key} className='hover bg-base-200/50'>
|
||||
<td>
|
||||
<div className='pl-4'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={selectedFiles.has(file.file_key)}
|
||||
onChange={(e) =>
|
||||
handleSelectFile(file.file_key, e.target.checked)
|
||||
}
|
||||
disabled={loading}
|
||||
className='checkbox checkbox-sm'
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className='max-w-0 !ps-0 sm:w-[80%]'>
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-base-content/80 text-xs'>
|
||||
{getFileName(file.file_key)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className='hidden sm:table-cell'>
|
||||
{formatFileSize(file.file_size)}
|
||||
</td>
|
||||
<td className='hidden sm:table-cell'>{formatDate(file.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className='border-base-300 flex items-center justify-between border-t p-4'>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className='btn btn-sm'
|
||||
>
|
||||
{_('Previous')}
|
||||
</button>
|
||||
<span className='text-base-content text-sm'>
|
||||
{_('Page {{current}} of {{total}}', { current: currentPage, total: totalPages })}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className='btn btn-sm'
|
||||
>
|
||||
{_('Next')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Delete Modal */}
|
||||
{showConfirmDelete && (
|
||||
<div
|
||||
className={clsx('fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
|
||||
style={{
|
||||
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete {{count}} selected file(s)?', {
|
||||
count: selectedFiles.size,
|
||||
})}
|
||||
onCancel={() => {
|
||||
setShowConfirmDelete(false);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
handleDeleteSelected();
|
||||
setShowConfirmDelete(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StorageManager;
|
||||
@@ -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<CheckoutState>({
|
||||
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}
|
||||
/>
|
||||
|
||||
<UsageStats quotas={quotas} />
|
||||
{!showStorageManager && <UsageStats quotas={quotas} />}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-y-8 sm:px-6'>
|
||||
<PlansComparison
|
||||
availablePlans={availablePlans}
|
||||
userPlan={userProfilePlan}
|
||||
onSubscribe={appService.hasIAP ? handleIAPSubscribe : handleStripeSubscribe}
|
||||
/>
|
||||
</div>
|
||||
{showStorageManager ? (
|
||||
<div className='flex flex-col gap-y-8 px-6'>
|
||||
<StorageManager />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex flex-col gap-y-8 sm:px-6'>
|
||||
<PlansComparison
|
||||
availablePlans={availablePlans}
|
||||
userPlan={userProfilePlan}
|
||||
onSubscribe={appService.hasIAP ? handleIAPSubscribe : handleStripeSubscribe}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-8 px-6'>
|
||||
<AccountActions
|
||||
userPlan={userProfilePlan}
|
||||
onLogout={handleLogout}
|
||||
onResetPassword={handleResetPassword}
|
||||
onUpdateEmail={handleUpdateEmail}
|
||||
onConfirmDelete={handleDeleteWithMessage}
|
||||
onRestorePurchase={handleIAPRestorePurchase}
|
||||
onManageSubscription={handleManageSubscription}
|
||||
onManageStorage={handleManageStorage}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='flex flex-col gap-y-8 px-6'>
|
||||
<AccountActions
|
||||
userPlan={userProfilePlan}
|
||||
onLogout={handleLogout}
|
||||
onResetPassword={handleResetPassword}
|
||||
onUpdateEmail={handleUpdateEmail}
|
||||
onConfirmDelete={handleDeleteWithMessage}
|
||||
onRestorePurchase={handleIAPRestorePurchase}
|
||||
onManageSubscription={handleManageSubscription}
|
||||
/>
|
||||
</div>
|
||||
<LegalLinks />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<StorageStats> => {
|
||||
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<ListFilesResponse> => {
|
||||
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<PurgeFilesResult> => {
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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<string | null, { count: number; size: number }>();
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user