feat(backup): include global settings in backup zip (#4211)
* feat(backup): include global settings in backup zip Backup zips previously held only book files and library.json. Issue #4098 asks for app configuration to be backed up too. A `settings.json` snapshot is now written at the zip root. Restore deep-merges it onto the current device's settings, so fields the snapshot omits keep their current values. `sanitizeSettingsForBackup` strips, via a blacklist, fields that are device-specific or sync/migration bookkeeping (filesystem paths, replica/kosync device ids, sync cursors, lastOpenBooks, screen brightness, schema versions). Account credentials (kosync/Readwise/ Hardcover tokens, AI gateway key, OPDS catalog logins) are stripped unless the user opts in via a new "Include account credentials" checkbox in the Backup & Restore dialog — the zip is unencrypted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(backup): keep revived books visible after a cloud-synced restore When the library is deleted (soft delete) and the deletion has synced to the cloud, restoring an older backup un-deletes the books locally — but the next sync's last-writer-wins merge re-applied the cloud's deletion tombstone, so the restored books vanished again. The deletion never bumps `updatedAt`, so a restored book and its cloud tombstone share the same timestamp; `processOldBook` breaks the tie toward the cloud. `reviveRestoredBooks` now fixes up books that were soft-deleted locally but present in the backup: - Bumps `updatedAt` so the restore out-ranks the cloud tombstone. A single uniform offset is applied to every revived book, so their relative order — and the library's "Updated" sort — is preserved exactly; the newest maps to now, none land in the future. - Clears `syncedAt` so the next push re-uploads them and corrects the cloud rows. - Restores `downloadedAt` / `coverDownloadedAt` from the backup record (the local deletion had cleared them) so revived books are not shown as not-downloaded even though their files were re-extracted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1115,7 +1115,6 @@
|
||||
"Select Backup": "اختر نسخة احتياطية",
|
||||
"Restore failed: {{error}}": "فشل الاستعادة: {{error}}",
|
||||
"Backup & Restore": "النسخ الاحتياطي والاستعادة",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "أنشئ نسخة احتياطية من مكتبتك أو استعد من نسخة احتياطية سابقة. ستدمج الاستعادة مع مكتبتك الحالية.",
|
||||
"Backup Library": "نسخ المكتبة احتياطيًا",
|
||||
"Restore Library": "استعادة المكتبة",
|
||||
"Creating backup...": "جارٍ إنشاء النسخة الاحتياطية...",
|
||||
@@ -1123,7 +1122,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} من {{total}} عنصر",
|
||||
"Backup completed successfully!": "تم النسخ الاحتياطي بنجاح!",
|
||||
"Restore completed successfully!": "تمت الاستعادة بنجاح!",
|
||||
"Your library has been saved to the selected location.": "تم حفظ مكتبتك في الموقع المحدد.",
|
||||
"{{added}} books added, {{updated}} books updated.": "تمت إضافة {{added}} كتب، وتحديث {{updated}} كتب.",
|
||||
"Operation failed": "فشلت العملية",
|
||||
"Loading library...": "جارٍ تحميل المكتبة...",
|
||||
@@ -1564,5 +1562,9 @@
|
||||
"No new annotations to import": "لا توجد تعليقات جديدة للاستيراد",
|
||||
"Import from Moon+ Reader": "استيراد من Moon+ Reader",
|
||||
"Character Mode": "وضع الحروف",
|
||||
"Highlight Word": "تمييز الكلمة"
|
||||
"Highlight Word": "تمييز الكلمة",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "أنشئ نسخة احتياطية من مكتبتك وإعداداتك أو استعد من نسخة احتياطية سابقة. سيتم دمج الاستعادة مع مكتبتك الحالية.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "تضمين بيانات اعتماد الحساب (رموز المزامنة، كلمات المرور). ملف النسخة الاحتياطية غير مشفّر.",
|
||||
"Your library and settings have been saved to the selected location.": "تم حفظ مكتبتك وإعداداتك في الموقع المحدد.",
|
||||
"Settings have been restored.": "تمت استعادة الإعدادات."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "ব্যাকআপ নির্বাচন করুন",
|
||||
"Restore failed: {{error}}": "পুনরুদ্ধার ব্যর্থ: {{error}}",
|
||||
"Backup & Restore": "ব্যাকআপ ও পুনরুদ্ধার",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "আপনার লাইব্রেরির ব্যাকআপ তৈরি করুন বা পূর্ববর্তী ব্যাকআপ থেকে পুনরুদ্ধার করুন। পুনরুদ্ধার আপনার বর্তমান লাইব্রেরির সাথে মার্জ হবে।",
|
||||
"Backup Library": "লাইব্রেরি ব্যাকআপ",
|
||||
"Restore Library": "লাইব্রেরি পুনরুদ্ধার",
|
||||
"Creating backup...": "ব্যাকআপ তৈরি হচ্ছে...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} আইটেম",
|
||||
"Backup completed successfully!": "ব্যাকআপ সফলভাবে সম্পন্ন!",
|
||||
"Restore completed successfully!": "পুনরুদ্ধার সফলভাবে সম্পন্ন!",
|
||||
"Your library has been saved to the selected location.": "আপনার লাইব্রেরি নির্বাচিত স্থানে সংরক্ষিত হয়েছে।",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}টি বই যোগ হয়েছে, {{updated}}টি বই আপডেট হয়েছে।",
|
||||
"Operation failed": "অপারেশন ব্যর্থ",
|
||||
"Loading library...": "লাইব্রেরি লোড হচ্ছে...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "ইম্পোর্ট করার জন্য কোনো নতুন টীকা নেই",
|
||||
"Import from Moon+ Reader": "Moon+ Reader থেকে ইম্পোর্ট করুন",
|
||||
"Character Mode": "অক্ষর মোড",
|
||||
"Highlight Word": "শব্দ হাইলাইট"
|
||||
"Highlight Word": "শব্দ হাইলাইট",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "আপনার লাইব্রেরি এবং সেটিংসের একটি ব্যাকআপ তৈরি করুন বা পূর্ববর্তী ব্যাকআপ থেকে পুনরুদ্ধার করুন। পুনরুদ্ধার করলে তা আপনার বর্তমান লাইব্রেরির সাথে মিশে যাবে।",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "অ্যাকাউন্ট শংসাপত্র (সিঙ্ক টোকেন, পাসওয়ার্ড) অন্তর্ভুক্ত করুন। ব্যাকআপ ফাইলটি এনক্রিপ্ট করা নেই।",
|
||||
"Your library and settings have been saved to the selected location.": "আপনার লাইব্রেরি এবং সেটিংস নির্বাচিত স্থানে সংরক্ষণ করা হয়েছে।",
|
||||
"Settings have been restored.": "সেটিংস পুনরুদ্ধার করা হয়েছে।"
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "གྲབས་ཉར་འདེམས་པ",
|
||||
"Restore failed: {{error}}": "སླར་གསོ་བྱེད་མ་ཐུབ: {{error}}",
|
||||
"Backup & Restore": "གྲབས་ཉར་དང་སླར་གསོ",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ཁྱོད་ཀྱི་དཔེ་མཛོད་ཀྱི་གྲབས་ཉར་བཟོ་བའམ་སྔོན་གྱི་གྲབས་ཉར་ནས་སླར་གསོ་བྱོས། སླར་གསོ་བྱས་ན་ཁྱོད་ཀྱི་ད་ལྟའི་དཔེ་མཛོད་དང་སྤྲོད་རེས་བྱེད།",
|
||||
"Backup Library": "དཔེ་མཛོད་གྲབས་ཉར",
|
||||
"Restore Library": "དཔེ་མཛོད་སླར་གསོ",
|
||||
"Creating backup...": "གྲབས་ཉར་བཟོ་བཞིན་པ...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} རྣམ་གྲངས",
|
||||
"Backup completed successfully!": "གྲབས་ཉར་ལེགས་གྲུབ་བྱུང་!",
|
||||
"Restore completed successfully!": "སླར་གསོ་ལེགས་གྲུབ་བྱུང་!",
|
||||
"Your library has been saved to the selected location.": "ཁྱོད་ཀྱི་དཔེ་མཛོད་འདེམས་ས་དེར་ཉར་ཟིན།",
|
||||
"{{added}} books added, {{updated}} books updated.": "དཔེ་དེབ {{added}} བསྣན་ཟིན, {{updated}} གསར་བསྒྱུར་བྱས་ཟིན།",
|
||||
"Operation failed": "བཀོལ་སྤྱོད་བྱེད་མ་ཐུབ",
|
||||
"Loading library...": "དཔེ་མཛོད་འཇུག་བཞིན་པ...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "ནང་འདྲེན་བྱ་རྒྱུའི་མཆན་འགྲེལ་གསར་པ་མེད།",
|
||||
"Import from Moon+ Reader": "Moon+ Reader ནས་ནང་འདྲེན།",
|
||||
"Character Mode": "ཡིག་འབྲུའི་སྒྲིག་སྟངས།",
|
||||
"Highlight Word": "ཐ་སྙད་གསལ་སྟོན།"
|
||||
"Highlight Word": "ཐ་སྙད་གསལ་སྟོན།",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "ཁྱེད་ཀྱི་དཔེ་མཛོད་དང་སྒྲིག་འགོད་ཀྱི་ཉར་ཚགས་ཤིག་བཟོ་བའམ་སྔོན་གྱི་ཉར་ཚགས་ཤིག་ནས་སོར་ཆུད་བྱེད། སོར་ཆུད་བྱས་ཚེ་ད་ལྟའི་དཔེ་མཛོད་དང་ཟླ་སྒྲིལ་འགྲོ།",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "རྩིས་ཐོའི་ངོས་སྦྱོར་ཆ་འཕྲིན་ (ཟུང་སྒྲིག་མཚོན་རྟགས། གསང་ཨང་) ཚུད་པར་བྱེད། ཉར་ཚགས་ཡིག་ཆ་གསང་སྦས་མ་བྱས།",
|
||||
"Your library and settings have been saved to the selected location.": "ཁྱེད་ཀྱི་དཔེ་མཛོད་དང་སྒྲིག་འགོད་འདེམས་པའི་གནས་སར་ཉར་ཚགས་བྱས་ཟིན།",
|
||||
"Settings have been restored.": "སྒྲིག་འགོད་སོར་ཆུད་ཟིན།"
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "Sicherung auswählen",
|
||||
"Restore failed: {{error}}": "Wiederherstellung fehlgeschlagen: {{error}}",
|
||||
"Backup & Restore": "Sichern & Wiederherstellen",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Erstellen Sie eine Sicherung Ihrer Bibliothek oder stellen Sie eine frühere Sicherung wieder her. Die Wiederherstellung wird mit Ihrer aktuellen Bibliothek zusammengeführt.",
|
||||
"Backup Library": "Bibliothek sichern",
|
||||
"Restore Library": "Bibliothek wiederherstellen",
|
||||
"Creating backup...": "Sicherung wird erstellt...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} von {{total}} Elementen",
|
||||
"Backup completed successfully!": "Sicherung erfolgreich abgeschlossen!",
|
||||
"Restore completed successfully!": "Wiederherstellung erfolgreich abgeschlossen!",
|
||||
"Your library has been saved to the selected location.": "Ihre Bibliothek wurde am ausgewählten Ort gespeichert.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} Bücher hinzugefügt, {{updated}} Bücher aktualisiert.",
|
||||
"Operation failed": "Vorgang fehlgeschlagen",
|
||||
"Loading library...": "Bibliothek wird geladen...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Keine neuen Anmerkungen zum Importieren",
|
||||
"Import from Moon+ Reader": "Aus Moon+ Reader importieren",
|
||||
"Character Mode": "Zeichenmodus",
|
||||
"Highlight Word": "Wort hervorheben"
|
||||
"Highlight Word": "Wort hervorheben",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Erstellen Sie eine Sicherung Ihrer Bibliothek und Einstellungen oder stellen Sie eine frühere Sicherung wieder her. Die Wiederherstellung wird mit Ihrer aktuellen Bibliothek zusammengeführt.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Anmeldedaten einschließen (Sync-Tokens, Passwörter). Die Sicherungsdatei ist nicht verschlüsselt.",
|
||||
"Your library and settings have been saved to the selected location.": "Ihre Bibliothek und Einstellungen wurden am ausgewählten Ort gespeichert.",
|
||||
"Settings have been restored.": "Einstellungen wurden wiederhergestellt."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "Επιλογή αντιγράφου ασφαλείας",
|
||||
"Restore failed: {{error}}": "Η επαναφορά απέτυχε: {{error}}",
|
||||
"Backup & Restore": "Αντίγραφα ασφαλείας & Επαναφορά",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Δημιουργήστε αντίγραφο ασφαλείας της βιβλιοθήκης σας ή επαναφέρετε από προηγούμενο αντίγραφο. Η επαναφορά θα συγχωνευθεί με την τρέχουσα βιβλιοθήκη σας.",
|
||||
"Backup Library": "Αντίγραφο βιβλιοθήκης",
|
||||
"Restore Library": "Επαναφορά βιβλιοθήκης",
|
||||
"Creating backup...": "Δημιουργία αντιγράφου ασφαλείας...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} από {{total}} στοιχεία",
|
||||
"Backup completed successfully!": "Το αντίγραφο ασφαλείας ολοκληρώθηκε!",
|
||||
"Restore completed successfully!": "Η επαναφορά ολοκληρώθηκε!",
|
||||
"Your library has been saved to the selected location.": "Η βιβλιοθήκη σας αποθηκεύτηκε στην επιλεγμένη τοποθεσία.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} βιβλία προστέθηκαν, {{updated}} βιβλία ενημερώθηκαν.",
|
||||
"Operation failed": "Η λειτουργία απέτυχε",
|
||||
"Loading library...": "Φόρτωση βιβλιοθήκης...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Δεν υπάρχουν νέα σχόλια για εισαγωγή",
|
||||
"Import from Moon+ Reader": "Εισαγωγή από Moon+ Reader",
|
||||
"Character Mode": "Λειτουργία χαρακτήρων",
|
||||
"Highlight Word": "Επισήμανση λέξης"
|
||||
"Highlight Word": "Επισήμανση λέξης",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Δημιουργήστε ένα αντίγραφο ασφαλείας της βιβλιοθήκης και των ρυθμίσεών σας ή κάντε επαναφορά από προηγούμενο αντίγραφο ασφαλείας. Η επαναφορά θα συγχωνευτεί με την τρέχουσα βιβλιοθήκη σας.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Συμπερίληψη διαπιστευτηρίων λογαριασμού (διακριτικά συγχρονισμού, κωδικοί πρόσβασης). Το αρχείο αντιγράφου ασφαλείας δεν είναι κρυπτογραφημένο.",
|
||||
"Your library and settings have been saved to the selected location.": "Η βιβλιοθήκη και οι ρυθμίσεις σας αποθηκεύτηκαν στην επιλεγμένη τοποθεσία.",
|
||||
"Settings have been restored.": "Οι ρυθμίσεις επαναφέρθηκαν."
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,6 @@
|
||||
"Select Backup": "Seleccionar copia de seguridad",
|
||||
"Restore failed: {{error}}": "Error en la restauración: {{error}}",
|
||||
"Backup & Restore": "Copia de seguridad y restauración",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crea una copia de seguridad de tu biblioteca o restaura desde una copia anterior. La restauración se fusionará con tu biblioteca actual.",
|
||||
"Backup Library": "Hacer copia de seguridad",
|
||||
"Restore Library": "Restaurar biblioteca",
|
||||
"Creating backup...": "Creando copia de seguridad...",
|
||||
@@ -1087,7 +1086,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} de {{total}} elementos",
|
||||
"Backup completed successfully!": "¡Copia de seguridad completada!",
|
||||
"Restore completed successfully!": "¡Restauración completada!",
|
||||
"Your library has been saved to the selected location.": "Tu biblioteca se ha guardado en la ubicación seleccionada.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} libros añadidos, {{updated}} libros actualizados.",
|
||||
"Operation failed": "La operación falló",
|
||||
"Loading library...": "Cargando biblioteca...",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "No hay anotaciones nuevas para importar",
|
||||
"Import from Moon+ Reader": "Importar desde Moon+ Reader",
|
||||
"Character Mode": "Modo de caracteres",
|
||||
"Highlight Word": "Resaltar palabra"
|
||||
"Highlight Word": "Resaltar palabra",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Crea una copia de seguridad de tu biblioteca y ajustes o restaura desde una copia anterior. La restauración se combinará con tu biblioteca actual.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Incluir credenciales de cuenta (tokens de sincronización, contraseñas). El archivo de copia de seguridad no está cifrado.",
|
||||
"Your library and settings have been saved to the selected location.": "Tu biblioteca y ajustes se han guardado en la ubicación seleccionada.",
|
||||
"Settings have been restored.": "Se han restaurado los ajustes."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "انتخاب پشتیبان",
|
||||
"Restore failed: {{error}}": "بازیابی ناموفق: {{error}}",
|
||||
"Backup & Restore": "پشتیبانگیری و بازیابی",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "از کتابخانه خود پشتیبان بگیرید یا از پشتیبان قبلی بازیابی کنید. بازیابی با کتابخانه فعلی شما ادغام خواهد شد.",
|
||||
"Backup Library": "پشتیبانگیری کتابخانه",
|
||||
"Restore Library": "بازیابی کتابخانه",
|
||||
"Creating backup...": "در حال ایجاد پشتیبان...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} از {{total}} مورد",
|
||||
"Backup completed successfully!": "پشتیبانگیری با موفقیت انجام شد!",
|
||||
"Restore completed successfully!": "بازیابی با موفقیت انجام شد!",
|
||||
"Your library has been saved to the selected location.": "کتابخانه شما در مکان انتخابشده ذخیره شد.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} کتاب اضافه شد، {{updated}} کتاب بهروزرسانی شد.",
|
||||
"Operation failed": "عملیات ناموفق",
|
||||
"Loading library...": "در حال بارگذاری کتابخانه...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "یادداشت جدیدی برای وارد کردن وجود ندارد",
|
||||
"Import from Moon+ Reader": "وارد کردن از Moon+ Reader",
|
||||
"Character Mode": "حالت نویسه",
|
||||
"Highlight Word": "برجستهسازی واژه"
|
||||
"Highlight Word": "برجستهسازی واژه",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "از کتابخانه و تنظیمات خود نسخه پشتیبان تهیه کنید یا از یک نسخه پشتیبان قبلی بازیابی کنید. بازیابی با کتابخانه فعلی شما ادغام میشود.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "گنجاندن اطلاعات ورود حساب (توکنهای همگامسازی، گذرواژهها). فایل پشتیبان رمزگذاری نشده است.",
|
||||
"Your library and settings have been saved to the selected location.": "کتابخانه و تنظیمات شما در محل انتخابشده ذخیره شد.",
|
||||
"Settings have been restored.": "تنظیمات بازیابی شد."
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,6 @@
|
||||
"Select Backup": "Sélectionner une sauvegarde",
|
||||
"Restore failed: {{error}}": "Échec de la restauration : {{error}}",
|
||||
"Backup & Restore": "Sauvegarde et restauration",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Créez une sauvegarde de votre bibliothèque ou restaurez à partir d'une sauvegarde précédente. La restauration fusionnera avec votre bibliothèque actuelle.",
|
||||
"Backup Library": "Sauvegarder la bibliothèque",
|
||||
"Restore Library": "Restaurer la bibliothèque",
|
||||
"Creating backup...": "Création de la sauvegarde...",
|
||||
@@ -1087,7 +1086,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} sur {{total}} éléments",
|
||||
"Backup completed successfully!": "Sauvegarde terminée avec succès !",
|
||||
"Restore completed successfully!": "Restauration terminée avec succès !",
|
||||
"Your library has been saved to the selected location.": "Votre bibliothèque a été enregistrée à l'emplacement sélectionné.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} livres ajoutés, {{updated}} livres mis à jour.",
|
||||
"Operation failed": "L'opération a échoué",
|
||||
"Loading library...": "Chargement de la bibliothèque...",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "Aucune nouvelle annotation à importer",
|
||||
"Import from Moon+ Reader": "Importer depuis Moon+ Reader",
|
||||
"Character Mode": "Mode caractère",
|
||||
"Highlight Word": "Surligner le mot"
|
||||
"Highlight Word": "Surligner le mot",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Créez une sauvegarde de votre bibliothèque et de vos paramètres, ou restaurez à partir d’une sauvegarde précédente. La restauration fusionnera avec votre bibliothèque actuelle.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Inclure les identifiants du compte (jetons de synchronisation, mots de passe). Le fichier de sauvegarde n’est pas chiffré.",
|
||||
"Your library and settings have been saved to the selected location.": "Votre bibliothèque et vos paramètres ont été enregistrés à l’emplacement sélectionné.",
|
||||
"Settings have been restored.": "Les paramètres ont été restaurés."
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,6 @@
|
||||
"Select Backup": "בחר גיבוי",
|
||||
"Restore failed: {{error}}": "השחזור נכשל: {{error}}",
|
||||
"Backup & Restore": "גיבוי ושחזור",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "צור גיבוי של הספרייה שלך או שחזר מגיבוי קודם. השחזור ימוזג עם הספרייה הנוכחית שלך.",
|
||||
"Backup Library": "גיבוי ספרייה",
|
||||
"Restore Library": "שחזור ספרייה",
|
||||
"Creating backup...": "יוצר גיבוי...",
|
||||
@@ -1087,7 +1086,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} מתוך {{total}} פריטים",
|
||||
"Backup completed successfully!": "הגיבוי הושלם בהצלחה!",
|
||||
"Restore completed successfully!": "השחזור הושלם בהצלחה!",
|
||||
"Your library has been saved to the selected location.": "הספרייה שלך נשמרה במיקום שנבחר.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} ספרים נוספו, {{updated}} ספרים עודכנו.",
|
||||
"Operation failed": "הפעולה נכשלה",
|
||||
"Loading library...": "טוען ספרייה...",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "אין הערות חדשות לייבוא",
|
||||
"Import from Moon+ Reader": "ייבוא מ-Moon+ Reader",
|
||||
"Character Mode": "מצב תווים",
|
||||
"Highlight Word": "הדגשת מילה"
|
||||
"Highlight Word": "הדגשת מילה",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "צור גיבוי של הספרייה וההגדרות שלך או שחזר מגיבוי קודם. השחזור ימוזג עם הספרייה הנוכחית שלך.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "כלול את פרטי הכניסה לחשבון (אסימוני סנכרון, סיסמאות). קובץ הגיבוי אינו מוצפן.",
|
||||
"Your library and settings have been saved to the selected location.": "הספרייה וההגדרות שלך נשמרו במיקום שנבחר.",
|
||||
"Settings have been restored.": "ההגדרות שוחזרו."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "बैकअप चुनें",
|
||||
"Restore failed: {{error}}": "पुनर्स्थापना विफल: {{error}}",
|
||||
"Backup & Restore": "बैकअप और पुनर्स्थापना",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "अपनी लाइब्रेरी का बैकअप बनाएं या पिछले बैकअप से पुनर्स्थापित करें। पुनर्स्थापना आपकी वर्तमान लाइब्रेरी के साथ मर्ज होगी।",
|
||||
"Backup Library": "लाइब्रेरी बैकअप",
|
||||
"Restore Library": "लाइब्रेरी पुनर्स्थापित करें",
|
||||
"Creating backup...": "बैकअप बनाया जा रहा है...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} आइटम",
|
||||
"Backup completed successfully!": "बैकअप सफलतापूर्वक पूर्ण!",
|
||||
"Restore completed successfully!": "पुनर्स्थापना सफलतापूर्वक पूर्ण!",
|
||||
"Your library has been saved to the selected location.": "आपकी लाइब्रेरी चयनित स्थान पर सहेजी गई है।",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} पुस्तकें जोड़ी गईं, {{updated}} पुस्तकें अपडेट की गईं।",
|
||||
"Operation failed": "ऑपरेशन विफल",
|
||||
"Loading library...": "लाइब्रेरी लोड हो रही है...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "आयात करने के लिए कोई नया एनोटेशन नहीं",
|
||||
"Import from Moon+ Reader": "Moon+ Reader से आयात करें",
|
||||
"Character Mode": "वर्ण मोड",
|
||||
"Highlight Word": "शब्द हाइलाइट करें"
|
||||
"Highlight Word": "शब्द हाइलाइट करें",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "अपनी लाइब्रेरी और सेटिंग्स का बैकअप बनाएं या किसी पिछले बैकअप से पुनर्स्थापित करें। पुनर्स्थापित करने पर यह आपकी वर्तमान लाइब्रेरी के साथ मर्ज हो जाएगा।",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "खाता क्रेडेंशियल (सिंक टोकन, पासवर्ड) शामिल करें। बैकअप फ़ाइल एन्क्रिप्टेड नहीं है।",
|
||||
"Your library and settings have been saved to the selected location.": "आपकी लाइब्रेरी और सेटिंग्स चयनित स्थान पर सहेज दी गई हैं।",
|
||||
"Settings have been restored.": "सेटिंग्स पुनर्स्थापित कर दी गई हैं।"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"Select Backup": "Biztonsági mentés kiválasztása",
|
||||
"Restore failed: {{error}}": "Visszaállítás sikertelen: {{error}}",
|
||||
"Backup & Restore": "Biztonsági mentés és visszaállítás",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Készítsen biztonsági mentést könyvtáráról, vagy állítsa vissza egy korábbi mentésből. A visszaállítás egyesítésre kerül a jelenlegi könyvtárával.",
|
||||
"Backup Library": "Könyvtár mentése",
|
||||
"Restore Library": "Könyvtár visszaállítása",
|
||||
"Creating backup...": "Biztonsági mentés létrehozása...",
|
||||
@@ -56,7 +55,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}}/{{total}} elem",
|
||||
"Backup completed successfully!": "Biztonsági mentés sikeresen elkészült!",
|
||||
"Restore completed successfully!": "Visszaállítás sikeresen befejeződött!",
|
||||
"Your library has been saved to the selected location.": "Könyvtára a kiválasztott helyre lett mentve.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} könyv hozzáadva, {{updated}} könyv frissítve.",
|
||||
"Operation failed": "A művelet sikertelen",
|
||||
"Close": "Bezárás",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Nincs új importálható jegyzet",
|
||||
"Import from Moon+ Reader": "Importálás Moon+ Reader programból",
|
||||
"Character Mode": "Karakter mód",
|
||||
"Highlight Word": "Szó kiemelése"
|
||||
"Highlight Word": "Szó kiemelése",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Készíts biztonsági mentést a könyvtáradról és a beállításaidról, vagy állítsd vissza egy korábbi biztonsági mentésből. A visszaállítás összevonásra kerül a jelenlegi könyvtáraddal.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Fiók hitelesítő adatainak (szinkronizációs tokenek, jelszavak) felvétele. A biztonsági mentés fájlja nincs titkosítva.",
|
||||
"Your library and settings have been saved to the selected location.": "A könyvtárad és a beállításaid a kiválasztott helyre lettek mentve.",
|
||||
"Settings have been restored.": "A beállítások visszaállítva."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "Pilih Cadangan",
|
||||
"Restore failed: {{error}}": "Pemulihan gagal: {{error}}",
|
||||
"Backup & Restore": "Cadangkan & Pulihkan",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Buat cadangan perpustakaan Anda atau pulihkan dari cadangan sebelumnya. Pemulihan akan digabungkan dengan perpustakaan Anda saat ini.",
|
||||
"Backup Library": "Cadangkan Perpustakaan",
|
||||
"Restore Library": "Pulihkan Perpustakaan",
|
||||
"Creating backup...": "Membuat cadangan...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} dari {{total}} item",
|
||||
"Backup completed successfully!": "Pencadangan berhasil!",
|
||||
"Restore completed successfully!": "Pemulihan berhasil!",
|
||||
"Your library has been saved to the selected location.": "Perpustakaan Anda telah disimpan di lokasi yang dipilih.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambahkan, {{updated}} buku diperbarui.",
|
||||
"Operation failed": "Operasi gagal",
|
||||
"Loading library...": "Memuat perpustakaan...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "Tidak ada anotasi baru untuk diimpor",
|
||||
"Import from Moon+ Reader": "Impor dari Moon+ Reader",
|
||||
"Character Mode": "Mode Karakter",
|
||||
"Highlight Word": "Sorot Kata"
|
||||
"Highlight Word": "Sorot Kata",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Buat cadangan pustaka dan pengaturan Anda atau pulihkan dari cadangan sebelumnya. Pemulihan akan digabungkan dengan pustaka Anda saat ini.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Sertakan kredensial akun (token sinkronisasi, kata sandi). File cadangan tidak dienkripsi.",
|
||||
"Your library and settings have been saved to the selected location.": "Pustaka dan pengaturan Anda telah disimpan ke lokasi yang dipilih.",
|
||||
"Settings have been restored.": "Pengaturan telah dipulihkan."
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,6 @@
|
||||
"Select Backup": "Seleziona backup",
|
||||
"Restore failed: {{error}}": "Ripristino fallito: {{error}}",
|
||||
"Backup & Restore": "Backup e ripristino",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crea un backup della tua libreria o ripristina da un backup precedente. Il ripristino verrà unito alla tua libreria attuale.",
|
||||
"Backup Library": "Backup libreria",
|
||||
"Restore Library": "Ripristina libreria",
|
||||
"Creating backup...": "Creazione backup...",
|
||||
@@ -1087,7 +1086,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} di {{total}} elementi",
|
||||
"Backup completed successfully!": "Backup completato con successo!",
|
||||
"Restore completed successfully!": "Ripristino completato con successo!",
|
||||
"Your library has been saved to the selected location.": "La tua libreria è stata salvata nella posizione selezionata.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} libri aggiunti, {{updated}} libri aggiornati.",
|
||||
"Operation failed": "Operazione fallita",
|
||||
"Loading library...": "Caricamento libreria...",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "Nessuna nuova annotazione da importare",
|
||||
"Import from Moon+ Reader": "Importa da Moon+ Reader",
|
||||
"Character Mode": "Modalità carattere",
|
||||
"Highlight Word": "Evidenzia parola"
|
||||
"Highlight Word": "Evidenzia parola",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Crea un backup della tua libreria e delle impostazioni o ripristina da un backup precedente. Il ripristino verrà unito alla tua libreria attuale.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Includi le credenziali dell’account (token di sincronizzazione, password). Il file di backup non è crittografato.",
|
||||
"Your library and settings have been saved to the selected location.": "La tua libreria e le impostazioni sono state salvate nel percorso selezionato.",
|
||||
"Settings have been restored.": "Le impostazioni sono state ripristinate."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "バックアップを選択",
|
||||
"Restore failed: {{error}}": "復元に失敗しました: {{error}}",
|
||||
"Backup & Restore": "バックアップと復元",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ライブラリのバックアップを作成するか、以前のバックアップから復元します。復元は現在のライブラリとマージされます。",
|
||||
"Backup Library": "ライブラリをバックアップ",
|
||||
"Restore Library": "ライブラリを復元",
|
||||
"Creating backup...": "バックアップを作成中...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 件",
|
||||
"Backup completed successfully!": "バックアップが完了しました!",
|
||||
"Restore completed successfully!": "復元が完了しました!",
|
||||
"Your library has been saved to the selected location.": "ライブラリが選択した場所に保存されました。",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}冊追加、{{updated}}冊更新されました。",
|
||||
"Operation failed": "操作に失敗しました",
|
||||
"Loading library...": "ライブラリを読み込み中...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "インポートする新しい注釈はありません",
|
||||
"Import from Moon+ Reader": "Moon+ Reader からインポート",
|
||||
"Character Mode": "文字モード",
|
||||
"Highlight Word": "単語を強調"
|
||||
"Highlight Word": "単語を強調",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "ライブラリと設定のバックアップを作成するか、以前のバックアップから復元します。復元すると、現在のライブラリと統合されます。",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "アカウントの認証情報(同期トークン、パスワード)を含めます。バックアップファイルは暗号化されません。",
|
||||
"Your library and settings have been saved to the selected location.": "ライブラリと設定が選択した場所に保存されました。",
|
||||
"Settings have been restored.": "設定が復元されました。"
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "백업 선택",
|
||||
"Restore failed: {{error}}": "복원 실패: {{error}}",
|
||||
"Backup & Restore": "백업 및 복원",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "라이브러리를 백업하거나 이전 백업에서 복원하세요. 복원 시 현재 라이브러리와 병합됩니다.",
|
||||
"Backup Library": "라이브러리 백업",
|
||||
"Restore Library": "라이브러리 복원",
|
||||
"Creating backup...": "백업 생성 중...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 항목",
|
||||
"Backup completed successfully!": "백업이 완료되었습니다!",
|
||||
"Restore completed successfully!": "복원이 완료되었습니다!",
|
||||
"Your library has been saved to the selected location.": "라이브러리가 선택한 위치에 저장되었습니다.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}}권 추가, {{updated}}권 업데이트되었습니다.",
|
||||
"Operation failed": "작업 실패",
|
||||
"Loading library...": "라이브러리 로딩 중...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "가져올 새 주석이 없습니다",
|
||||
"Import from Moon+ Reader": "Moon+ Reader에서 가져오기",
|
||||
"Character Mode": "글자 모드",
|
||||
"Highlight Word": "단어 강조"
|
||||
"Highlight Word": "단어 강조",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "라이브러리와 설정의 백업을 만들거나 이전 백업에서 복원합니다. 복원하면 현재 라이브러리와 병합됩니다.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "계정 자격 증명(동기화 토큰, 비밀번호)을 포함합니다. 백업 파일은 암호화되지 않습니다.",
|
||||
"Your library and settings have been saved to the selected location.": "라이브러리와 설정이 선택한 위치에 저장되었습니다.",
|
||||
"Settings have been restored.": "설정이 복원되었습니다."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "Pilih Sandaran",
|
||||
"Restore failed: {{error}}": "Pemulihan gagal: {{error}}",
|
||||
"Backup & Restore": "Sandaran & Pemulihan",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Buat sandaran perpustakaan anda atau pulihkan daripada sandaran sebelumnya. Pemulihan akan digabungkan dengan perpustakaan semasa anda.",
|
||||
"Backup Library": "Sandarkan Perpustakaan",
|
||||
"Restore Library": "Pulihkan Perpustakaan",
|
||||
"Creating backup...": "Membuat sandaran...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} daripada {{total}} item",
|
||||
"Backup completed successfully!": "Sandaran berjaya!",
|
||||
"Restore completed successfully!": "Pemulihan berjaya!",
|
||||
"Your library has been saved to the selected location.": "Perpustakaan anda telah disimpan di lokasi yang dipilih.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambah, {{updated}} buku dikemas kini.",
|
||||
"Operation failed": "Operasi gagal",
|
||||
"Loading library...": "Memuatkan perpustakaan...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "Tiada anotasi baharu untuk diimport",
|
||||
"Import from Moon+ Reader": "Import dari Moon+ Reader",
|
||||
"Character Mode": "Mod Aksara",
|
||||
"Highlight Word": "Serlah Perkataan"
|
||||
"Highlight Word": "Serlah Perkataan",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Cipta sandaran pustaka dan tetapan anda atau pulihkan daripada sandaran terdahulu. Pemulihan akan digabungkan dengan pustaka semasa anda.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Sertakan kelayakan akaun (token penyegerakan, kata laluan). Fail sandaran tidak disulitkan.",
|
||||
"Your library and settings have been saved to the selected location.": "Pustaka dan tetapan anda telah disimpan ke lokasi yang dipilih.",
|
||||
"Settings have been restored.": "Tetapan telah dipulihkan."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "Selecteer back-up",
|
||||
"Restore failed: {{error}}": "Herstel mislukt: {{error}}",
|
||||
"Backup & Restore": "Back-up & Herstel",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Maak een back-up van uw bibliotheek of herstel vanuit een eerdere back-up. Herstel wordt samengevoegd met uw huidige bibliotheek.",
|
||||
"Backup Library": "Bibliotheek back-uppen",
|
||||
"Restore Library": "Bibliotheek herstellen",
|
||||
"Creating backup...": "Back-up maken...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} van {{total}} items",
|
||||
"Backup completed successfully!": "Back-up succesvol voltooid!",
|
||||
"Restore completed successfully!": "Herstel succesvol voltooid!",
|
||||
"Your library has been saved to the selected location.": "Uw bibliotheek is opgeslagen op de geselecteerde locatie.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} boeken toegevoegd, {{updated}} boeken bijgewerkt.",
|
||||
"Operation failed": "Bewerking mislukt",
|
||||
"Loading library...": "Bibliotheek laden...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Geen nieuwe annotaties om te importeren",
|
||||
"Import from Moon+ Reader": "Importeren uit Moon+ Reader",
|
||||
"Character Mode": "Tekenmodus",
|
||||
"Highlight Word": "Woord markeren"
|
||||
"Highlight Word": "Woord markeren",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Maak een back-up van je bibliotheek en instellingen of herstel vanuit een eerdere back-up. Bij herstellen wordt samengevoegd met je huidige bibliotheek.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Accountgegevens opnemen (synchronisatietokens, wachtwoorden). Het back-upbestand is niet versleuteld.",
|
||||
"Your library and settings have been saved to the selected location.": "Je bibliotheek en instellingen zijn opgeslagen op de geselecteerde locatie.",
|
||||
"Settings have been restored.": "Instellingen zijn hersteld."
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,6 @@
|
||||
"Select Backup": "Wybierz kopię zapasową",
|
||||
"Restore failed: {{error}}": "Przywracanie nie powiodło się: {{error}}",
|
||||
"Backup & Restore": "Kopia zapasowa i przywracanie",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Utwórz kopię zapasową biblioteki lub przywróć z poprzedniej kopii. Przywracanie zostanie połączone z bieżącą biblioteką.",
|
||||
"Backup Library": "Kopia zapasowa biblioteki",
|
||||
"Restore Library": "Przywróć bibliotekę",
|
||||
"Creating backup...": "Tworzenie kopii zapasowej...",
|
||||
@@ -1099,7 +1098,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} z {{total}} elementów",
|
||||
"Backup completed successfully!": "Kopia zapasowa ukończona!",
|
||||
"Restore completed successfully!": "Przywracanie ukończone!",
|
||||
"Your library has been saved to the selected location.": "Biblioteka została zapisana w wybranej lokalizacji.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Dodano {{added}} książek, zaktualizowano {{updated}} książek.",
|
||||
"Operation failed": "Operacja nie powiodła się",
|
||||
"Loading library...": "Wczytywanie biblioteki...",
|
||||
@@ -1508,5 +1506,9 @@
|
||||
"No new annotations to import": "Brak nowych adnotacji do zaimportowania",
|
||||
"Import from Moon+ Reader": "Importuj z Moon+ Reader",
|
||||
"Character Mode": "Tryb znaków",
|
||||
"Highlight Word": "Podświetl słowo"
|
||||
"Highlight Word": "Podświetl słowo",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Utwórz kopię zapasową swojej biblioteki i ustawień lub przywróć z poprzedniej kopii zapasowej. Przywracanie zostanie scalone z bieżącą biblioteką.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Uwzględnij dane logowania do konta (tokeny synchronizacji, hasła). Plik kopii zapasowej nie jest zaszyfrowany.",
|
||||
"Your library and settings have been saved to the selected location.": "Twoja biblioteka i ustawienia zostały zapisane w wybranej lokalizacji.",
|
||||
"Settings have been restored.": "Ustawienia zostały przywrócone."
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"Select Backup": "Selecionar backup",
|
||||
"Restore failed: {{error}}": "Falha na restauração: {{error}}",
|
||||
"Backup & Restore": "Backup e restauração",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crie um backup da sua biblioteca ou restaure a partir de um backup anterior. A restauração será mesclada com sua biblioteca atual.",
|
||||
"Backup Library": "Fazer backup da biblioteca",
|
||||
"Restore Library": "Restaurar biblioteca",
|
||||
"Creating backup...": "Criando backup...",
|
||||
@@ -56,7 +55,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} de {{total}} itens",
|
||||
"Backup completed successfully!": "Backup concluído com sucesso!",
|
||||
"Restore completed successfully!": "Restauração concluída com sucesso!",
|
||||
"Your library has been saved to the selected location.": "Sua biblioteca foi salva no local selecionado.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} livros adicionados, {{updated}} livros atualizados.",
|
||||
"Operation failed": "A operação falhou",
|
||||
"Close": "Fechar",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "Não há novas anotações para importar",
|
||||
"Import from Moon+ Reader": "Importar do Moon+ Reader",
|
||||
"Character Mode": "Modo de caracteres",
|
||||
"Highlight Word": "Destacar palavra"
|
||||
"Highlight Word": "Destacar palavra",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Crie um backup da sua biblioteca e configurações ou restaure a partir de um backup anterior. A restauração será mesclada com a sua biblioteca atual.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Incluir credenciais da conta (tokens de sincronização, senhas). O arquivo de backup não é criptografado.",
|
||||
"Your library and settings have been saved to the selected location.": "Sua biblioteca e configurações foram salvas no local selecionado.",
|
||||
"Settings have been restored.": "As configurações foram restauradas."
|
||||
}
|
||||
|
||||
@@ -1079,7 +1079,6 @@
|
||||
"Select Backup": "Selecionar backup",
|
||||
"Restore failed: {{error}}": "Falha na restauração: {{error}}",
|
||||
"Backup & Restore": "Backup e restauração",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Crie um backup da sua biblioteca ou restaure a partir de um backup anterior. A restauração será mesclada com sua biblioteca atual.",
|
||||
"Backup Library": "Fazer backup da biblioteca",
|
||||
"Restore Library": "Restaurar biblioteca",
|
||||
"Creating backup...": "Criando backup...",
|
||||
@@ -1087,7 +1086,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} de {{total}} itens",
|
||||
"Backup completed successfully!": "Backup concluído com sucesso!",
|
||||
"Restore completed successfully!": "Restauração concluída com sucesso!",
|
||||
"Your library has been saved to the selected location.": "Sua biblioteca foi salva no local selecionado.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} livros adicionados, {{updated}} livros atualizados.",
|
||||
"Operation failed": "Operação falhou",
|
||||
"Loading library...": "Carregando biblioteca...",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "Não há novas anotações para importar",
|
||||
"Import from Moon+ Reader": "Importar do Moon+ Reader",
|
||||
"Character Mode": "Modo de caracteres",
|
||||
"Highlight Word": "Destacar palavra"
|
||||
"Highlight Word": "Destacar palavra",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Crie uma cópia de segurança da sua biblioteca e definições ou restaure a partir de uma cópia de segurança anterior. A restauração será combinada com a sua biblioteca atual.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Incluir credenciais da conta (tokens de sincronização, palavras-passe). O ficheiro de cópia de segurança não está encriptado.",
|
||||
"Your library and settings have been saved to the selected location.": "A sua biblioteca e definições foram guardadas na localização selecionada.",
|
||||
"Settings have been restored.": "As definições foram restauradas."
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"Select Backup": "Selectați Backup",
|
||||
"Restore failed: {{error}}": "Restaurarea a eșuat: {{eroare}}",
|
||||
"Backup & Restore": "Backup și restaurare",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Creați o copie de rezervă a bibliotecii dvs. sau restaurați dintr-o copie de rezervă anterioară. Restaurarea se va îmbina cu biblioteca dvs. actuală.",
|
||||
"Backup Library": "Bibliotecă de rezervă",
|
||||
"Restore Library": "Restaurați biblioteca",
|
||||
"Creating backup...": "Se creează backup...",
|
||||
@@ -56,7 +55,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} din {{total}} articole",
|
||||
"Backup completed successfully!": "Backup finalizat cu succes!",
|
||||
"Restore completed successfully!": "Restaurare finalizată cu succes!",
|
||||
"Your library has been saved to the selected location.": "Biblioteca dvs. a fost salvată în locația selectată.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} cărți adăugate, {{updated}} cărți actualizate.",
|
||||
"Operation failed": "Operațiunea a eșuat",
|
||||
"Close": "Închide",
|
||||
@@ -1480,5 +1478,9 @@
|
||||
"No new annotations to import": "Nicio adnotare nouă de importat",
|
||||
"Import from Moon+ Reader": "Importă din Moon+ Reader",
|
||||
"Character Mode": "Mod caracter",
|
||||
"Highlight Word": "Evidențiere cuvânt"
|
||||
"Highlight Word": "Evidențiere cuvânt",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Creează o copie de rezervă a bibliotecii și a setărilor tale sau restaurează dintr-o copie de rezervă anterioară. Restaurarea se va îmbina cu biblioteca ta curentă.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Include datele de autentificare ale contului (jetoane de sincronizare, parole). Fișierul de rezervă nu este criptat.",
|
||||
"Your library and settings have been saved to the selected location.": "Biblioteca și setările tale au fost salvate în locația selectată.",
|
||||
"Settings have been restored.": "Setările au fost restaurate."
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,6 @@
|
||||
"Select Backup": "Выбрать резервную копию",
|
||||
"Restore failed: {{error}}": "Ошибка восстановления: {{error}}",
|
||||
"Backup & Restore": "Резервное копирование и восстановление",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Создайте резервную копию библиотеки или восстановите из предыдущей копии. Восстановление будет объединено с текущей библиотекой.",
|
||||
"Backup Library": "Резервная копия библиотеки",
|
||||
"Restore Library": "Восстановить библиотеку",
|
||||
"Creating backup...": "Создание резервной копии...",
|
||||
@@ -1099,7 +1098,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} из {{total}} элементов",
|
||||
"Backup completed successfully!": "Резервное копирование завершено!",
|
||||
"Restore completed successfully!": "Восстановление завершено!",
|
||||
"Your library has been saved to the selected location.": "Ваша библиотека сохранена в выбранном месте.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Добавлено {{added}} книг, обновлено {{updated}} книг.",
|
||||
"Operation failed": "Операция не удалась",
|
||||
"Loading library...": "Загрузка библиотеки...",
|
||||
@@ -1508,5 +1506,9 @@
|
||||
"No new annotations to import": "Нет новых аннотаций для импорта",
|
||||
"Import from Moon+ Reader": "Импорт из Moon+ Reader",
|
||||
"Character Mode": "Посимвольный режим",
|
||||
"Highlight Word": "Подсветка слова"
|
||||
"Highlight Word": "Подсветка слова",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Создайте резервную копию своей библиотеки и настроек или восстановите из предыдущей резервной копии. Восстановление будет объединено с вашей текущей библиотекой.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Включить учётные данные аккаунта (токены синхронизации, пароли). Файл резервной копии не зашифрован.",
|
||||
"Your library and settings have been saved to the selected location.": "Ваша библиотека и настройки сохранены в выбранном месте.",
|
||||
"Settings have been restored.": "Настройки восстановлены."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "උපස්ථයක් තෝරන්න",
|
||||
"Restore failed: {{error}}": "ප්රතිසාධනය අසාර්ථකයි: {{error}}",
|
||||
"Backup & Restore": "උපස්ථ සහ ප්රතිසාධනය",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "ඔබේ පුස්තකාලයේ උපස්ථයක් සාදන්න හෝ පෙර උපස්ථයකින් ප්රතිසාධනය කරන්න. ප්රතිසාධනය ඔබේ වත්මන් පුස්තකාලය සමග ඒකාබද්ධ වේ.",
|
||||
"Backup Library": "පුස්තකාලය උපස්ථ කරන්න",
|
||||
"Restore Library": "පුස්තකාලය ප්රතිසාධනය කරන්න",
|
||||
"Creating backup...": "උපස්ථය සාදමින්...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} අයිතම",
|
||||
"Backup completed successfully!": "උපස්ථය සාර්ථකව සම්පූර්ණයි!",
|
||||
"Restore completed successfully!": "ප්රතිසාධනය සාර්ථකව සම්පූර්ණයි!",
|
||||
"Your library has been saved to the selected location.": "ඔබේ පුස්තකාලය තෝරාගත් ස්ථානයේ සුරැකිණි.",
|
||||
"{{added}} books added, {{updated}} books updated.": "පොත් {{added}}ක් එකතු විය, {{updated}}ක් යාවත්කාලීන විය.",
|
||||
"Operation failed": "මෙහෙයුම අසාර්ථකයි",
|
||||
"Loading library...": "පුස්තකාලය පූරණය වෙමින්...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "ආයාත කිරීමට නව සටහන් නැත",
|
||||
"Import from Moon+ Reader": "Moon+ Reader වෙතින් ආයාත කරන්න",
|
||||
"Character Mode": "අක්ෂර ප්රකාරය",
|
||||
"Highlight Word": "වචනය උද්දීපනය"
|
||||
"Highlight Word": "වචනය උද්දීපනය",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "ඔබගේ පුස්තකාලයේ සහ සැකසීම්වල උපස්ථයක් සාදන්න හෝ පෙර උපස්ථයකින් ප්රතිස්ථාපනය කරන්න. ප්රතිස්ථාපනය ඔබගේ වත්මන් පුස්තකාලය සමඟ ඒකාබද්ධ වේ.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "ගිණුම් අක්තපත්ර (සමමුහුර්ත ටෝකන, මුරපද) ඇතුළත් කරන්න. උපස්ථ ගොනුව සංකේතනය කර නැත.",
|
||||
"Your library and settings have been saved to the selected location.": "ඔබගේ පුස්තකාලය සහ සැකසීම් තෝරාගත් ස්ථානයට සුරැකී ඇත.",
|
||||
"Settings have been restored.": "සැකසීම් ප්රතිස්ථාපනය කර ඇත."
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,6 @@
|
||||
"Select Backup": "Izberi varnostno kopijo",
|
||||
"Restore failed: {{error}}": "Obnovitev ni uspela: {{error}}",
|
||||
"Backup & Restore": "Varnostno kopiranje in obnovitev",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Ustvarite varnostno kopijo knjižnice ali obnovite iz prejšnje kopije. Obnovitev bo združena z vašo trenutno knjižnico.",
|
||||
"Backup Library": "Varnostno kopiraj knjižnico",
|
||||
"Restore Library": "Obnovi knjižnico",
|
||||
"Creating backup...": "Ustvarjanje varnostne kopije...",
|
||||
@@ -1099,7 +1098,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} od {{total}} elementov",
|
||||
"Backup completed successfully!": "Varnostno kopiranje uspešno!",
|
||||
"Restore completed successfully!": "Obnovitev uspešno zaključena!",
|
||||
"Your library has been saved to the selected location.": "Vaša knjižnica je shranjena na izbrani lokaciji.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} knjig dodanih, {{updated}} knjig posodobljenih.",
|
||||
"Operation failed": "Operacija ni uspela",
|
||||
"Loading library...": "Nalaganje knjižnice...",
|
||||
@@ -1508,5 +1506,9 @@
|
||||
"No new annotations to import": "Ni novih opomb za uvoz",
|
||||
"Import from Moon+ Reader": "Uvozi iz Moon+ Reader",
|
||||
"Character Mode": "Znakovni način",
|
||||
"Highlight Word": "Označi besedo"
|
||||
"Highlight Word": "Označi besedo",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Ustvarite varnostno kopijo svoje knjižnice in nastavitev ali jo obnovite iz prejšnje varnostne kopije. Obnovitev se bo združila z vašo trenutno knjižnico.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Vključi poverilnice računa (žetone za sinhronizacijo, gesla). Datoteka varnostne kopije ni šifrirana.",
|
||||
"Your library and settings have been saved to the selected location.": "Vaša knjižnica in nastavitve so bile shranjene na izbrano mesto.",
|
||||
"Settings have been restored.": "Nastavitve so bile obnovljene."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "Välj säkerhetskopia",
|
||||
"Restore failed: {{error}}": "Återställning misslyckades: {{error}}",
|
||||
"Backup & Restore": "Säkerhetskopiera & Återställ",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Skapa en säkerhetskopia av ditt bibliotek eller återställ från en tidigare kopia. Återställningen slås samman med ditt nuvarande bibliotek.",
|
||||
"Backup Library": "Säkerhetskopiera bibliotek",
|
||||
"Restore Library": "Återställ bibliotek",
|
||||
"Creating backup...": "Skapar säkerhetskopia...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} av {{total}} objekt",
|
||||
"Backup completed successfully!": "Säkerhetskopieringen slutförd!",
|
||||
"Restore completed successfully!": "Återställningen slutförd!",
|
||||
"Your library has been saved to the selected location.": "Ditt bibliotek har sparats på den valda platsen.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} böcker tillagda, {{updated}} böcker uppdaterade.",
|
||||
"Operation failed": "Åtgärden misslyckades",
|
||||
"Loading library...": "Laddar bibliotek...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Inga nya anteckningar att importera",
|
||||
"Import from Moon+ Reader": "Importera från Moon+ Reader",
|
||||
"Character Mode": "Teckenläge",
|
||||
"Highlight Word": "Markera ord"
|
||||
"Highlight Word": "Markera ord",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Skapa en säkerhetskopia av ditt bibliotek och dina inställningar eller återställ från en tidigare säkerhetskopia. Återställning slås samman med ditt nuvarande bibliotek.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Inkludera kontouppgifter (synkroniseringstoken, lösenord). Säkerhetskopian är inte krypterad.",
|
||||
"Your library and settings have been saved to the selected location.": "Ditt bibliotek och dina inställningar har sparats på den valda platsen.",
|
||||
"Settings have been restored.": "Inställningarna har återställts."
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "காப்புப்பிரதியைத் தேர்ந்தெடுக்கவும்",
|
||||
"Restore failed: {{error}}": "மீட்டமைப்பு தோல்வி: {{error}}",
|
||||
"Backup & Restore": "காப்புப்பிரதி & மீட்டமைப்பு",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "உங்கள் நூலகத்தின் காப்புப்பிரதியை உருவாக்கவும் அல்லது முந்தைய காப்புப்பிரதியிலிருந்து மீட்டமைக்கவும். மீட்டமைப்பு உங்கள் தற்போதைய நூலகத்துடன் இணைக்கப்படும்.",
|
||||
"Backup Library": "நூலகத்தை காப்புப்பிரதி எடு",
|
||||
"Restore Library": "நூலகத்தை மீட்டமை",
|
||||
"Creating backup...": "காப்புப்பிரதி உருவாக்கப்படுகிறது...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} உருப்படிகள்",
|
||||
"Backup completed successfully!": "காப்புப்பிரதி வெற்றிகரமாக முடிந்தது!",
|
||||
"Restore completed successfully!": "மீட்டமைப்பு வெற்றிகரமாக முடிந்தது!",
|
||||
"Your library has been saved to the selected location.": "உங்கள் நூலகம் தேர்ந்தெடுக்கப்பட்ட இடத்தில் சேமிக்கப்பட்டது.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} புத்தகங்கள் சேர்க்கப்பட்டன, {{updated}} புத்தகங்கள் புதுப்பிக்கப்பட்டன.",
|
||||
"Operation failed": "செயல்பாடு தோல்வி",
|
||||
"Loading library...": "நூலகம் ஏற்றப்படுகிறது...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "இறக்குமதி செய்ய புதிய குறிப்புகள் இல்லை",
|
||||
"Import from Moon+ Reader": "Moon+ Reader இலிருந்து இறக்குமதி செய்",
|
||||
"Character Mode": "எழுத்து முறை",
|
||||
"Highlight Word": "சொல்லை முன்னிலைப்படுத்து"
|
||||
"Highlight Word": "சொல்லை முன்னிலைப்படுத்து",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "உங்கள் நூலகம் மற்றும் அமைப்புகளின் காப்புப்பிரதியை உருவாக்கவும் அல்லது முந்தைய காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். மீட்டெடுப்பது உங்கள் தற்போதைய நூலகத்துடன் இணைக்கப்படும்.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "கணக்கு அறிமுகச் சான்றுகளை (ஒத்திசைவு டோக்கன்கள், கடவுச்சொற்கள்) சேர்க்கவும். காப்புப்பிரதி கோப்பு குறியாக்கம் செய்யப்படவில்லை.",
|
||||
"Your library and settings have been saved to the selected location.": "உங்கள் நூலகம் மற்றும் அமைப்புகள் தேர்ந்தெடுக்கப்பட்ட இடத்தில் சேமிக்கப்பட்டன.",
|
||||
"Settings have been restored.": "அமைப்புகள் மீட்டெடுக்கப்பட்டன."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "เลือกข้อมูลสำรอง",
|
||||
"Restore failed: {{error}}": "การกู้คืนล้มเหลว: {{error}}",
|
||||
"Backup & Restore": "สำรองและกู้คืน",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "สร้างข้อมูลสำรองของห้องสมุดหรือกู้คืนจากข้อมูลสำรองก่อนหน้า การกู้คืนจะรวมกับห้องสมุดปัจจุบันของคุณ",
|
||||
"Backup Library": "สำรองห้องสมุด",
|
||||
"Restore Library": "กู้คืนห้องสมุด",
|
||||
"Creating backup...": "กำลังสร้างข้อมูลสำรอง...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} จาก {{total}} รายการ",
|
||||
"Backup completed successfully!": "สำรองข้อมูลสำเร็จ!",
|
||||
"Restore completed successfully!": "กู้คืนสำเร็จ!",
|
||||
"Your library has been saved to the selected location.": "ห้องสมุดของคุณถูกบันทึกไปยังตำแหน่งที่เลือก",
|
||||
"{{added}} books added, {{updated}} books updated.": "เพิ่ม {{added}} เล่ม, อัปเดต {{updated}} เล่ม",
|
||||
"Operation failed": "การดำเนินการล้มเหลว",
|
||||
"Loading library...": "กำลังโหลดห้องสมุด...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "ไม่มีคำอธิบายประกอบใหม่ให้นำเข้า",
|
||||
"Import from Moon+ Reader": "นำเข้าจาก Moon+ Reader",
|
||||
"Character Mode": "โหมดอักขระ",
|
||||
"Highlight Word": "เน้นคำ"
|
||||
"Highlight Word": "เน้นคำ",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "สร้างข้อมูลสำรองของคลังหนังสือและการตั้งค่าของคุณ หรือกู้คืนจากข้อมูลสำรองก่อนหน้า การกู้คืนจะรวมเข้ากับคลังหนังสือปัจจุบันของคุณ",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "รวมข้อมูลรับรองบัญชี (โทเค็นการซิงค์ รหัสผ่าน) ไฟล์ข้อมูลสำรองไม่ได้เข้ารหัส",
|
||||
"Your library and settings have been saved to the selected location.": "คลังหนังสือและการตั้งค่าของคุณถูกบันทึกไปยังตำแหน่งที่เลือกแล้ว",
|
||||
"Settings have been restored.": "กู้คืนการตั้งค่าแล้ว"
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,6 @@
|
||||
"Select Backup": "Yedek Seç",
|
||||
"Restore failed: {{error}}": "Geri yükleme başarısız: {{error}}",
|
||||
"Backup & Restore": "Yedekleme & Geri Yükleme",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Kütüphanenizin yedeğini oluşturun veya önceki bir yedekten geri yükleyin. Geri yükleme mevcut kütüphanenizle birleştirilecektir.",
|
||||
"Backup Library": "Kütüphaneyi Yedekle",
|
||||
"Restore Library": "Kütüphaneyi Geri Yükle",
|
||||
"Creating backup...": "Yedek oluşturuluyor...",
|
||||
@@ -1075,7 +1074,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} öğe",
|
||||
"Backup completed successfully!": "Yedekleme başarıyla tamamlandı!",
|
||||
"Restore completed successfully!": "Geri yükleme başarıyla tamamlandı!",
|
||||
"Your library has been saved to the selected location.": "Kütüphaneniz seçilen konuma kaydedildi.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} kitap eklendi, {{updated}} kitap güncellendi.",
|
||||
"Operation failed": "İşlem başarısız",
|
||||
"Loading library...": "Kütüphane yükleniyor...",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "İçe aktarılacak yeni açıklama yok",
|
||||
"Import from Moon+ Reader": "Moon+ Reader'dan içe aktar",
|
||||
"Character Mode": "Karakter modu",
|
||||
"Highlight Word": "Kelimeyi vurgula"
|
||||
"Highlight Word": "Kelimeyi vurgula",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Kitaplığınızın ve ayarlarınızın bir yedeğini oluşturun veya önceki bir yedekten geri yükleyin. Geri yükleme, mevcut kitaplığınızla birleştirilir.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Hesap kimlik bilgilerini (eşitleme belirteçleri, parolalar) dahil et. Yedek dosyası şifrelenmemiştir.",
|
||||
"Your library and settings have been saved to the selected location.": "Kitaplığınız ve ayarlarınız seçilen konuma kaydedildi.",
|
||||
"Settings have been restored.": "Ayarlar geri yüklendi."
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,6 @@
|
||||
"Select Backup": "Обрати резервну копію",
|
||||
"Restore failed: {{error}}": "Помилка відновлення: {{error}}",
|
||||
"Backup & Restore": "Резервне копіювання та відновлення",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Створіть резервну копію бібліотеки або відновіть з попередньої копії. Відновлення буде об'єднано з поточною бібліотекою.",
|
||||
"Backup Library": "Резервна копія бібліотеки",
|
||||
"Restore Library": "Відновити бібліотеку",
|
||||
"Creating backup...": "Створення резервної копії...",
|
||||
@@ -1099,7 +1098,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} з {{total}} елементів",
|
||||
"Backup completed successfully!": "Резервне копіювання завершено!",
|
||||
"Restore completed successfully!": "Відновлення завершено!",
|
||||
"Your library has been saved to the selected location.": "Вашу бібліотеку збережено у вибраному місці.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Додано {{added}} книг, оновлено {{updated}} книг.",
|
||||
"Operation failed": "Операція не вдалася",
|
||||
"Loading library...": "Завантаження бібліотеки...",
|
||||
@@ -1508,5 +1506,9 @@
|
||||
"No new annotations to import": "Немає нових анотацій для імпорту",
|
||||
"Import from Moon+ Reader": "Імпорт із Moon+ Reader",
|
||||
"Character Mode": "Посимвольний режим",
|
||||
"Highlight Word": "Підсвічування слова"
|
||||
"Highlight Word": "Підсвічування слова",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Створіть резервну копію вашої бібліотеки та налаштувань або відновіть із попередньої резервної копії. Відновлення буде об’єднано з вашою поточною бібліотекою.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Включити облікові дані акаунта (токени синхронізації, паролі). Файл резервної копії не зашифровано.",
|
||||
"Your library and settings have been saved to the selected location.": "Вашу бібліотеку та налаштування збережено у вибраному місці.",
|
||||
"Settings have been restored.": "Налаштування відновлено."
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"Select Backup": "Zaxira nusxani tanlang",
|
||||
"Restore failed: {{error}}": "Tiklashda xato: {{error}}",
|
||||
"Backup & Restore": "Zaxira nusxa va tiklash",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Kutubxonangizning zaxira nusxasini yarating yoki oldingi zaxira nusxadan tiklang. Tiklash joriy kutubxonangiz bilan birlashtiriladi.",
|
||||
"Backup Library": "Kutubxonani zaxiralash",
|
||||
"Restore Library": "Kutubxonani tiklash",
|
||||
"Creating backup...": "Zaxira nusxa yaratilmoqda...",
|
||||
@@ -56,7 +55,6 @@
|
||||
"{{current}} of {{total}} items": "{{total}} elementdan {{current}}",
|
||||
"Backup completed successfully!": "Zaxira nusxa muvaffaqiyatli yaratildi!",
|
||||
"Restore completed successfully!": "Tiklash muvaffaqiyatli yakunlandi!",
|
||||
"Your library has been saved to the selected location.": "Kutubxonangiz tanlangan joyga saqlandi.",
|
||||
"{{added}} books added, {{updated}} books updated.": "{{added}} ta kitob qoʻshildi, {{updated}} ta kitob yangilandi.",
|
||||
"Operation failed": "Amal bajarilmadi",
|
||||
"Close": "Yopish",
|
||||
@@ -1452,5 +1450,9 @@
|
||||
"No new annotations to import": "Import qilish uchun yangi izohlar yoʻq",
|
||||
"Import from Moon+ Reader": "Moon+ Reader’dan import qilish",
|
||||
"Character Mode": "Belgi rejimi",
|
||||
"Highlight Word": "So'zni ajratish"
|
||||
"Highlight Word": "So'zni ajratish",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Kutubxonangiz va sozlamalaringizning zaxira nusxasini yarating yoki avvalgi zaxira nusxasidan tiklang. Tiklash joriy kutubxonangiz bilan birlashtiriladi.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Hisob hisob maʼlumotlarini (sinxronlash tokenlari, parollar) kiritish. Zaxira nusxa fayli shifrlanmagan.",
|
||||
"Your library and settings have been saved to the selected location.": "Kutubxonangiz va sozlamalaringiz tanlangan joyga saqlandi.",
|
||||
"Settings have been restored.": "Sozlamalar tiklandi."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "Chọn bản sao lưu",
|
||||
"Restore failed: {{error}}": "Khôi phục thất bại: {{error}}",
|
||||
"Backup & Restore": "Sao lưu & Khôi phục",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "Tạo bản sao lưu thư viện hoặc khôi phục từ bản sao lưu trước đó. Khôi phục sẽ hợp nhất với thư viện hiện tại của bạn.",
|
||||
"Backup Library": "Sao lưu thư viện",
|
||||
"Restore Library": "Khôi phục thư viện",
|
||||
"Creating backup...": "Đang tạo bản sao lưu...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} mục",
|
||||
"Backup completed successfully!": "Sao lưu thành công!",
|
||||
"Restore completed successfully!": "Khôi phục thành công!",
|
||||
"Your library has been saved to the selected location.": "Thư viện của bạn đã được lưu tại vị trí đã chọn.",
|
||||
"{{added}} books added, {{updated}} books updated.": "Đã thêm {{added}} sách, cập nhật {{updated}} sách.",
|
||||
"Operation failed": "Thao tác thất bại",
|
||||
"Loading library...": "Đang tải thư viện...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "Không có chú thích mới để nhập",
|
||||
"Import from Moon+ Reader": "Nhập từ Moon+ Reader",
|
||||
"Character Mode": "Chế độ ký tự",
|
||||
"Highlight Word": "Tô sáng từ"
|
||||
"Highlight Word": "Tô sáng từ",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "Tạo bản sao lưu thư viện và cài đặt của bạn hoặc khôi phục từ bản sao lưu trước đó. Việc khôi phục sẽ hợp nhất với thư viện hiện tại của bạn.",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "Bao gồm thông tin đăng nhập tài khoản (mã thông báo đồng bộ, mật khẩu). Tệp sao lưu không được mã hóa.",
|
||||
"Your library and settings have been saved to the selected location.": "Thư viện và cài đặt của bạn đã được lưu vào vị trí đã chọn.",
|
||||
"Settings have been restored.": "Đã khôi phục cài đặt."
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "选择备份",
|
||||
"Restore failed: {{error}}": "恢复失败:{{error}}",
|
||||
"Backup & Restore": "备份与恢复",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "创建书库备份或从之前的备份恢复。恢复将与当前书库合并。",
|
||||
"Backup Library": "备份书库",
|
||||
"Restore Library": "恢复书库",
|
||||
"Creating backup...": "正在创建备份...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 项",
|
||||
"Backup completed successfully!": "备份成功!",
|
||||
"Restore completed successfully!": "恢复成功!",
|
||||
"Your library has been saved to the selected location.": "您的书库已保存到所选位置。",
|
||||
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本书,更新 {{updated}} 本书。",
|
||||
"Operation failed": "操作失败",
|
||||
"Loading library...": "正在加载书库...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "没有可导入的新注释",
|
||||
"Import from Moon+ Reader": "从 Moon+ Reader 导入",
|
||||
"Character Mode": "单字模式",
|
||||
"Highlight Word": "高亮整词"
|
||||
"Highlight Word": "高亮整词",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "创建您的书库和设置的备份,或从之前的备份中恢复。恢复将与您当前的书库合并。",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "包含账户凭据(同步令牌、密码)。备份文件未加密。",
|
||||
"Your library and settings have been saved to the selected location.": "您的书库和设置已保存到所选位置。",
|
||||
"Settings have been restored.": "设置已恢复。"
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"Select Backup": "選擇備份",
|
||||
"Restore failed: {{error}}": "還原失敗:{{error}}",
|
||||
"Backup & Restore": "備份與還原",
|
||||
"Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.": "建立書庫備份或從先前的備份還原。還原將與目前的書庫合併。",
|
||||
"Backup Library": "備份書庫",
|
||||
"Restore Library": "還原書庫",
|
||||
"Creating backup...": "正在建立備份...",
|
||||
@@ -1063,7 +1062,6 @@
|
||||
"{{current}} of {{total}} items": "{{current}} / {{total}} 項",
|
||||
"Backup completed successfully!": "備份成功!",
|
||||
"Restore completed successfully!": "還原成功!",
|
||||
"Your library has been saved to the selected location.": "您的書庫已儲存至所選位置。",
|
||||
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本書,更新 {{updated}} 本書。",
|
||||
"Operation failed": "操作失敗",
|
||||
"Loading library...": "正在載入書庫...",
|
||||
@@ -1424,5 +1422,9 @@
|
||||
"No new annotations to import": "沒有可匯入的新註解",
|
||||
"Import from Moon+ Reader": "從 Moon+ Reader 匯入",
|
||||
"Character Mode": "單字模式",
|
||||
"Highlight Word": "高亮整詞"
|
||||
"Highlight Word": "高亮整詞",
|
||||
"Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.": "建立您的書庫與設定的備份,或從先前的備份還原。還原將與您目前的書庫合併。",
|
||||
"Include account credentials (sync tokens, passwords). The backup file is not encrypted.": "包含帳戶憑證(同步權杖、密碼)。備份檔案未加密。",
|
||||
"Your library and settings have been saved to the selected location.": "您的書庫與設定已儲存至所選位置。",
|
||||
"Settings have been restored.": "設定已還原。"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mergeBookConfigs, mergeBookMetadata } from '@/services/backupService';
|
||||
import {
|
||||
mergeBookConfigs,
|
||||
mergeBookMetadata,
|
||||
reviveRestoredBooks,
|
||||
type RevivedBook,
|
||||
} from '@/services/backupService';
|
||||
import { Book, BookConfig, BookNote } from '@/types/book';
|
||||
|
||||
function makeBook(overrides: Partial<Book> = {}): Book {
|
||||
@@ -181,3 +186,101 @@ describe('mergeBookMetadata', () => {
|
||||
expect(result.title).toBe('Current Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reviveRestoredBooks', () => {
|
||||
// A fixed "now" well past every fixture timestamp keeps assertions deterministic.
|
||||
const NOW = 1_000_000_000_000;
|
||||
|
||||
/** Pair a freshly-merged live record with its backup metadata. */
|
||||
function makeRevived(
|
||||
bookOverrides: Partial<Book> = {},
|
||||
backupOverrides: Partial<Book> = {},
|
||||
): RevivedBook {
|
||||
return {
|
||||
book: makeBook({ deletedAt: null, downloadedAt: null, ...bookOverrides }),
|
||||
backup: makeBook(backupOverrides),
|
||||
};
|
||||
}
|
||||
|
||||
it('does nothing for an empty list', () => {
|
||||
expect(() => reviveRestoredBooks([], NOW)).not.toThrow();
|
||||
});
|
||||
|
||||
it('preserves the relative updatedAt order of revived books', () => {
|
||||
const revived = [
|
||||
makeRevived({ hash: 'a', updatedAt: 100 }),
|
||||
makeRevived({ hash: 'b', updatedAt: 300 }),
|
||||
makeRevived({ hash: 'c', updatedAt: 200 }),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
const byHash = (h: string) => revived.find((r) => r.book.hash === h)!.book.updatedAt;
|
||||
// Original ascending order a < c < b must still hold.
|
||||
expect(byHash('a')).toBeLessThan(byHash('c'));
|
||||
expect(byHash('c')).toBeLessThan(byHash('b'));
|
||||
});
|
||||
|
||||
it('bumps every book strictly above its original (and cloud) timestamp', () => {
|
||||
const revived = [
|
||||
makeRevived({ hash: 'a', updatedAt: 100 }),
|
||||
makeRevived({ hash: 'b', updatedAt: 300 }),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[0]!.book.updatedAt).toBeGreaterThan(100);
|
||||
expect(revived[1]!.book.updatedAt).toBeGreaterThan(300);
|
||||
});
|
||||
|
||||
it('maps the newest revived book to exactly now', () => {
|
||||
const revived = [
|
||||
makeRevived({ hash: 'a', updatedAt: 100 }),
|
||||
makeRevived({ hash: 'b', updatedAt: 300 }),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(Math.max(revived[0]!.book.updatedAt, revived[1]!.book.updatedAt)).toBe(NOW);
|
||||
});
|
||||
|
||||
it('keeps the exact gap between books (single uniform offset)', () => {
|
||||
const revived = [
|
||||
makeRevived({ hash: 'a', updatedAt: 1000 }),
|
||||
makeRevived({ hash: 'b', updatedAt: 1500 }),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[1]!.book.updatedAt - revived[0]!.book.updatedAt).toBe(500);
|
||||
});
|
||||
|
||||
it('still bumps by at least 1 when a book has a future timestamp', () => {
|
||||
const future = NOW + 5000;
|
||||
const revived = [makeRevived({ hash: 'a', updatedAt: future })];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[0]!.book.updatedAt).toBe(future + 1);
|
||||
});
|
||||
|
||||
it('clears syncedAt so the next push re-uploads the book', () => {
|
||||
const revived = [makeRevived({ hash: 'a', updatedAt: 100, syncedAt: 999 })];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[0]!.book.syncedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('restores download state from the backup record', () => {
|
||||
const revived = [
|
||||
makeRevived(
|
||||
{ hash: 'a', updatedAt: 100, downloadedAt: null, coverDownloadedAt: null },
|
||||
{ downloadedAt: 555, coverDownloadedAt: 666 },
|
||||
),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[0]!.book.downloadedAt).toBe(555);
|
||||
expect(revived[0]!.book.coverDownloadedAt).toBe(666);
|
||||
});
|
||||
|
||||
it('falls back to now when the backup record has no download state', () => {
|
||||
const revived = [
|
||||
makeRevived(
|
||||
{ hash: 'a', updatedAt: 100, downloadedAt: null, coverDownloadedAt: null },
|
||||
{ downloadedAt: null, coverDownloadedAt: null },
|
||||
),
|
||||
];
|
||||
reviveRestoredBooks(revived, NOW);
|
||||
expect(revived[0]!.book.downloadedAt).toBe(NOW);
|
||||
expect(revived[0]!.book.coverDownloadedAt).toBe(NOW);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
BACKUP_SETTINGS_BLACKLIST,
|
||||
BACKUP_SETTINGS_CREDENTIAL_FIELDS,
|
||||
sanitizeSettingsForBackup,
|
||||
mergeRestoredSettings,
|
||||
} from '@/services/backupService';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
|
||||
/**
|
||||
* Tests for global-settings backup support (issue #4098):
|
||||
* - sanitizeSettingsForBackup strips device-specific / sync-bookkeeping
|
||||
* fields and (by default) credentials before writing settings.json.
|
||||
* - mergeRestoredSettings deep-merges a restored snapshot onto the
|
||||
* current device settings, preserving stripped fields.
|
||||
*/
|
||||
|
||||
/** Read an arbitrary (possibly stripped) field as a plain record. */
|
||||
const rec = (value: unknown): Record<string, unknown> => value as Record<string, unknown>;
|
||||
|
||||
function makeSettings(overrides: Partial<SystemSettings> = {}): SystemSettings {
|
||||
return {
|
||||
version: 5,
|
||||
migrationVersion: 3,
|
||||
localBooksDir: '/Users/me/Books',
|
||||
customRootDir: '/Users/me/readest',
|
||||
keepLogin: true,
|
||||
screenBrightness: 0.7,
|
||||
autoScreenBrightness: false,
|
||||
lastOpenBooks: ['book-1', 'book-2'],
|
||||
savedBookCoverForLockScreen: 'cover',
|
||||
savedBookCoverForLockScreenPath: '/Users/me/cover.png',
|
||||
libraryViewMode: 'grid',
|
||||
librarySortBy: 'title',
|
||||
libraryColumns: 4,
|
||||
replicaDeviceId: 'device-uuid-aaa',
|
||||
lastSyncedAtBooks: 1234,
|
||||
lastSyncedAtConfigs: 1235,
|
||||
lastSyncedAtNotes: 1236,
|
||||
lastSyncedAtReplicas: { book: 'hlc-1' },
|
||||
opdsCatalogs: [
|
||||
{
|
||||
id: 'cat-1',
|
||||
title: 'Cat',
|
||||
url: 'https://example.com/opds',
|
||||
username: 'opds-user',
|
||||
password: 'opds-pass',
|
||||
},
|
||||
],
|
||||
kosync: {
|
||||
enabled: true,
|
||||
serverUrl: 'https://kosync.example',
|
||||
username: 'kuser',
|
||||
userkey: 'kkey',
|
||||
password: 'kpass',
|
||||
deviceId: 'kosync-device-id',
|
||||
deviceName: 'My Phone',
|
||||
checksumMethod: 'binary',
|
||||
strategy: 'prompt',
|
||||
},
|
||||
readwise: { enabled: true, accessToken: 'rw-token', lastSyncedAt: 999 },
|
||||
hardcover: { enabled: false, accessToken: 'hc-token', lastSyncedAt: 888 },
|
||||
aiSettings: {
|
||||
enabled: true,
|
||||
provider: 'ollama',
|
||||
ollamaBaseUrl: 'http://localhost',
|
||||
aiGatewayApiKey: 'ai-secret-key',
|
||||
},
|
||||
globalReadSettings: {
|
||||
sideBarWidth: '20%',
|
||||
customThemes: [{ name: 'mytheme' }],
|
||||
},
|
||||
globalViewSettings: {
|
||||
userStylesheet: 'body { color: red }',
|
||||
uiLanguage: 'en',
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as SystemSettings;
|
||||
}
|
||||
|
||||
describe('sanitizeSettingsForBackup - blacklist', () => {
|
||||
it('strips device-specific filesystem paths', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings()));
|
||||
expect(out['localBooksDir']).toBeUndefined();
|
||||
expect(out['customRootDir']).toBeUndefined();
|
||||
expect(out['savedBookCoverForLockScreenPath']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips per-device identity fields', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings()));
|
||||
expect(out['replicaDeviceId']).toBeUndefined();
|
||||
expect(rec(out['kosync'])['deviceId']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips sync cursors', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings()));
|
||||
expect(out['lastSyncedAtBooks']).toBeUndefined();
|
||||
expect(out['lastSyncedAtConfigs']).toBeUndefined();
|
||||
expect(out['lastSyncedAtNotes']).toBeUndefined();
|
||||
expect(out['lastSyncedAtReplicas']).toBeUndefined();
|
||||
expect(rec(out['readwise'])['lastSyncedAt']).toBeUndefined();
|
||||
expect(rec(out['hardcover'])['lastSyncedAt']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips transient runtime state', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings()));
|
||||
expect(out['lastOpenBooks']).toBeUndefined();
|
||||
expect(out['screenBrightness']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips schema versioning fields', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings()));
|
||||
expect(out['version']).toBeUndefined();
|
||||
expect(out['migrationVersion']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps preferences, layout and customization fields', () => {
|
||||
const out = sanitizeSettingsForBackup(makeSettings());
|
||||
expect(out.keepLogin).toBe(true);
|
||||
expect(out.libraryViewMode).toBe('grid');
|
||||
expect(out.libraryColumns).toBe(4);
|
||||
expect(out.kosync.serverUrl).toBe('https://kosync.example');
|
||||
expect(out.kosync.deviceName).toBe('My Phone');
|
||||
expect(out.globalReadSettings.customThemes).toEqual([{ name: 'mytheme' }]);
|
||||
expect(out.globalViewSettings.userStylesheet).toBe('body { color: red }');
|
||||
});
|
||||
|
||||
it('does not mutate the input settings', () => {
|
||||
const input = makeSettings();
|
||||
const snapshot = JSON.stringify(input);
|
||||
sanitizeSettingsForBackup(input);
|
||||
expect(JSON.stringify(input)).toBe(snapshot);
|
||||
});
|
||||
|
||||
it('every blacklisted path is a string', () => {
|
||||
expect(BACKUP_SETTINGS_BLACKLIST.every((p) => typeof p === 'string')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeSettingsForBackup - credentials', () => {
|
||||
it('strips credentials by default', () => {
|
||||
const out = sanitizeSettingsForBackup(makeSettings());
|
||||
expect(rec(out.kosync)['username']).toBeUndefined();
|
||||
expect(rec(out.kosync)['userkey']).toBeUndefined();
|
||||
expect(rec(out.kosync)['password']).toBeUndefined();
|
||||
expect(rec(out.readwise)['accessToken']).toBeUndefined();
|
||||
expect(rec(out.hardcover)['accessToken']).toBeUndefined();
|
||||
expect(rec(out.aiSettings)['aiGatewayApiKey']).toBeUndefined();
|
||||
expect(out.opdsCatalogs[0]!.username).toBeUndefined();
|
||||
expect(out.opdsCatalogs[0]!.password).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps non-credential OPDS catalog fields when stripping credentials', () => {
|
||||
const out = sanitizeSettingsForBackup(makeSettings());
|
||||
expect(out.opdsCatalogs[0]!.id).toBe('cat-1');
|
||||
expect(out.opdsCatalogs[0]!.url).toBe('https://example.com/opds');
|
||||
});
|
||||
|
||||
it('keeps credentials when includeCredentials is true', () => {
|
||||
const out = sanitizeSettingsForBackup(makeSettings(), { includeCredentials: true });
|
||||
expect(out.kosync.password).toBe('kpass');
|
||||
expect(out.readwise.accessToken).toBe('rw-token');
|
||||
expect(out.hardcover.accessToken).toBe('hc-token');
|
||||
expect(rec(out.aiSettings)['aiGatewayApiKey']).toBe('ai-secret-key');
|
||||
expect(out.opdsCatalogs[0]!.username).toBe('opds-user');
|
||||
expect(out.opdsCatalogs[0]!.password).toBe('opds-pass');
|
||||
});
|
||||
|
||||
it('still strips blacklist fields even when credentials are included', () => {
|
||||
const out = rec(sanitizeSettingsForBackup(makeSettings(), { includeCredentials: true }));
|
||||
expect(out['localBooksDir']).toBeUndefined();
|
||||
expect(out['replicaDeviceId']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('every credential path is a string', () => {
|
||||
expect(BACKUP_SETTINGS_CREDENTIAL_FIELDS.every((p) => typeof p === 'string')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeRestoredSettings', () => {
|
||||
it('overrides current scalar values with the backup snapshot', () => {
|
||||
const current = makeSettings({ libraryViewMode: 'list', libraryColumns: 2 });
|
||||
const backup = sanitizeSettingsForBackup(makeSettings({ libraryViewMode: 'grid' }));
|
||||
const merged = mergeRestoredSettings(current, backup);
|
||||
expect(merged.libraryViewMode).toBe('grid');
|
||||
expect(merged.libraryColumns).toBe(4);
|
||||
});
|
||||
|
||||
it('preserves device-specific fields absent from the backup', () => {
|
||||
const current = makeSettings({
|
||||
localBooksDir: '/device/Books',
|
||||
version: 9,
|
||||
migrationVersion: 7,
|
||||
});
|
||||
const backup = sanitizeSettingsForBackup(makeSettings());
|
||||
const merged = mergeRestoredSettings(current, backup);
|
||||
expect(merged.localBooksDir).toBe('/device/Books');
|
||||
expect(merged.version).toBe(9);
|
||||
expect(merged.migrationVersion).toBe(7);
|
||||
expect(merged.replicaDeviceId).toBe('device-uuid-aaa');
|
||||
});
|
||||
|
||||
it('deep-merges nested objects, keeping current-only nested keys', () => {
|
||||
const current = makeSettings();
|
||||
const backup = sanitizeSettingsForBackup(
|
||||
makeSettings({
|
||||
kosync: {
|
||||
enabled: true,
|
||||
serverUrl: 'https://new-kosync.example',
|
||||
username: 'kuser',
|
||||
userkey: 'kkey',
|
||||
password: 'kpass',
|
||||
deviceId: 'kosync-device-id',
|
||||
deviceName: 'Restored Name',
|
||||
checksumMethod: 'binary',
|
||||
strategy: 'prompt',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const merged = mergeRestoredSettings(current, backup);
|
||||
// serverUrl/deviceName come from the backup, deviceId stays the device's own
|
||||
expect(merged.kosync.serverUrl).toBe('https://new-kosync.example');
|
||||
expect(merged.kosync.deviceName).toBe('Restored Name');
|
||||
expect(merged.kosync.deviceId).toBe('kosync-device-id');
|
||||
});
|
||||
|
||||
it('replaces arrays wholesale rather than concatenating', () => {
|
||||
const current = makeSettings({
|
||||
globalReadSettings: {
|
||||
customThemes: [{ name: 'device-theme-a' }, { name: 'device-theme-b' }],
|
||||
} as SystemSettings['globalReadSettings'],
|
||||
});
|
||||
const backup = sanitizeSettingsForBackup(
|
||||
makeSettings({
|
||||
globalReadSettings: {
|
||||
customThemes: [{ name: 'restored-theme' }],
|
||||
} as SystemSettings['globalReadSettings'],
|
||||
}),
|
||||
);
|
||||
const merged = mergeRestoredSettings(current, backup);
|
||||
expect(merged.globalReadSettings.customThemes).toEqual([{ name: 'restored-theme' }]);
|
||||
});
|
||||
|
||||
it('does not mutate the current or backup objects', () => {
|
||||
const current = makeSettings();
|
||||
const backup = sanitizeSettingsForBackup(makeSettings({ libraryViewMode: 'list' }));
|
||||
const currentSnapshot = JSON.stringify(current);
|
||||
const backupSnapshot = JSON.stringify(backup);
|
||||
mergeRestoredSettings(current, backup);
|
||||
expect(JSON.stringify(current)).toBe(currentSnapshot);
|
||||
expect(JSON.stringify(backup)).toBe(backupSnapshot);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ interface BackupResult {
|
||||
type: 'backup' | 'restore';
|
||||
booksAdded?: number;
|
||||
booksUpdated?: number;
|
||||
settingsRestored?: boolean;
|
||||
}
|
||||
|
||||
interface BackupWindowProps {
|
||||
@@ -51,12 +52,14 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
const [progress, setProgress] = useState<BackupProgress>({ current: 0, total: 0 });
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [result, setResult] = useState<BackupResult | null>(null);
|
||||
const [includeCredentials, setIncludeCredentials] = useState(false);
|
||||
|
||||
const resetState = () => {
|
||||
setStatus('idle');
|
||||
setProgress({ current: 0, total: 0 });
|
||||
setErrorMessage('');
|
||||
setResult(null);
|
||||
setIncludeCredentials(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,9 +92,14 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
try {
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const filename = `readest-backup-${timestamp}.zip`;
|
||||
const saved = await saveBackupFile(appService, filename, (current, total, currentFile) => {
|
||||
setProgress({ current, total, currentFile });
|
||||
});
|
||||
const saved = await saveBackupFile(
|
||||
appService,
|
||||
filename,
|
||||
{ includeCredentials },
|
||||
(current, total, currentFile) => {
|
||||
setProgress({ current, total, currentFile });
|
||||
},
|
||||
);
|
||||
if (saved) {
|
||||
setResult({ type: 'backup' });
|
||||
setStatus('completed');
|
||||
@@ -125,7 +133,7 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
? result.files[0].file
|
||||
: await appService.openFile(result.files[0]!.path!, 'None');
|
||||
|
||||
const { booksAdded, booksUpdated } = await restoreFromBackupZip(
|
||||
const { booksAdded, booksUpdated, settingsRestored } = await restoreFromBackupZip(
|
||||
appService,
|
||||
zipFile,
|
||||
(current, total, currentFile) => {
|
||||
@@ -140,6 +148,7 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
type: 'restore',
|
||||
booksAdded: Math.min(booksAdded, booksCount),
|
||||
booksUpdated: Math.min(booksUpdated, booksCount),
|
||||
settingsRestored,
|
||||
});
|
||||
setStatus('completed');
|
||||
onPullLibrary(true);
|
||||
@@ -180,10 +189,24 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-base-content/70 text-sm'>
|
||||
{_(
|
||||
'Create a backup of your library or restore from a previous backup. Restoring will merge with your current library.',
|
||||
'Create a backup of your library and settings or restore from a previous backup. Restoring will merge with your current library.',
|
||||
)}
|
||||
</p>
|
||||
|
||||
<label className='flex cursor-pointer items-start gap-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={includeCredentials}
|
||||
onChange={(e) => setIncludeCredentials(e.target.checked)}
|
||||
className='checkbox checkbox-sm mt-0.5 shrink-0'
|
||||
/>
|
||||
<span className='text-base-content/70 text-sm'>
|
||||
{_(
|
||||
'Include account credentials (sync tokens, passwords). The backup file is not encrypted.',
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button className='btn btn-outline w-full gap-2' onClick={handleBackup}>
|
||||
<RiUploadCloud2Line className='h-5 w-5' />
|
||||
{_('Backup Library')}
|
||||
@@ -251,11 +274,11 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
|
||||
<div className='bg-success/10 border-success/20 rounded-lg border p-3'>
|
||||
<p className='text-success/80 text-sm'>
|
||||
{result.type === 'backup'
|
||||
? _('Your library has been saved to the selected location.')
|
||||
? _('Your library and settings have been saved to the selected location.')
|
||||
: _('{{added}} books added, {{updated}} books updated.', {
|
||||
added: result.booksAdded ?? 0,
|
||||
updated: result.booksUpdated ?? 0,
|
||||
})}
|
||||
}) + (result.settingsRestored ? ' ' + _('Settings have been restored.') : '')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppService } from '@/types/system';
|
||||
import { EXTS } from '@/libs/document';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { Book, BookConfig, BookNote } from '@/types/book';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { getLibraryFilename } from '@/utils/book';
|
||||
import { stampBookConfigSchema } from '@/utils/serializer';
|
||||
import { configureZip } from '@/utils/zip';
|
||||
@@ -10,6 +11,141 @@ import { configureZip } from '@/utils/zip';
|
||||
/** Book file extensions for identifying book files in backup directories. */
|
||||
const BOOK_EXTS = new Set(Object.values(EXTS));
|
||||
|
||||
/** Root-level zip entry name for the backed-up global settings snapshot. */
|
||||
export const SETTINGS_BACKUP_FILENAME = 'settings.json';
|
||||
|
||||
/**
|
||||
* Options controlling what a backup zip includes.
|
||||
*/
|
||||
export interface BackupOptions {
|
||||
/**
|
||||
* Include account credentials (sync tokens, passwords, API keys) in the
|
||||
* settings snapshot. The backup zip is unencrypted, so this is opt-in and
|
||||
* defaults to false.
|
||||
*/
|
||||
includeCredentials?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemSettings dot-paths excluded from backups. Each is either tied to
|
||||
* this device (and meaningless to restore elsewhere) or sync/migration
|
||||
* bookkeeping that would corrupt state if restored stale. Restore keeps
|
||||
* the current device's value for every path here — see issue #4098.
|
||||
*/
|
||||
export const BACKUP_SETTINGS_BLACKLIST = [
|
||||
// Device filesystem paths — invalid on another device / OS.
|
||||
'localBooksDir',
|
||||
'customRootDir',
|
||||
'savedBookCoverForLockScreenPath',
|
||||
// Per-device identity — restoring causes sync identity / HLC collisions.
|
||||
'replicaDeviceId',
|
||||
'kosync.deviceId',
|
||||
// Sync cursors — stale values make sync skip pulls or re-push everything.
|
||||
'lastSyncedAtBooks',
|
||||
'lastSyncedAtConfigs',
|
||||
'lastSyncedAtNotes',
|
||||
'lastSyncedAtReplicas',
|
||||
'readwise.lastSyncedAt',
|
||||
'hardcover.lastSyncedAt',
|
||||
// Transient runtime state — book keys may not exist post-restore; screen
|
||||
// brightness is live device state.
|
||||
'lastOpenBooks',
|
||||
'screenBrightness',
|
||||
// Schema versioning — restore keeps the current device's value so its
|
||||
// migrations are not skipped.
|
||||
'version',
|
||||
'migrationVersion',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Credential dot-paths stripped from backups unless `includeCredentials`
|
||||
* is set. OPDS catalog credentials live inside the `opdsCatalogs` array
|
||||
* and are handled separately in `sanitizeSettingsForBackup`.
|
||||
*/
|
||||
export const BACKUP_SETTINGS_CREDENTIAL_FIELDS = [
|
||||
'kosync.username',
|
||||
'kosync.userkey',
|
||||
'kosync.password',
|
||||
'readwise.accessToken',
|
||||
'hardcover.accessToken',
|
||||
'aiSettings.aiGatewayApiKey',
|
||||
] as const;
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Delete a dot-path key from a deep object; no-op when the path is absent. */
|
||||
const deletePath = (obj: Record<string, unknown>, path: string): void => {
|
||||
const parts = path.split('.');
|
||||
let cur: Record<string, unknown> = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const next = cur[parts[i]!];
|
||||
if (!isPlainObject(next)) return;
|
||||
cur = next;
|
||||
}
|
||||
delete cur[parts[parts.length - 1]!];
|
||||
};
|
||||
|
||||
/**
|
||||
* Produce a copy of SystemSettings safe to write into a backup zip:
|
||||
* strips device-specific / sync-bookkeeping fields always, and account
|
||||
* credentials unless `includeCredentials` is set. Input is not mutated.
|
||||
*/
|
||||
export function sanitizeSettingsForBackup(
|
||||
settings: SystemSettings,
|
||||
options: BackupOptions = {},
|
||||
): SystemSettings {
|
||||
const clone = structuredClone(settings) as SystemSettings & Record<string, unknown>;
|
||||
for (const path of BACKUP_SETTINGS_BLACKLIST) {
|
||||
deletePath(clone, path);
|
||||
}
|
||||
if (!options.includeCredentials) {
|
||||
for (const path of BACKUP_SETTINGS_CREDENTIAL_FIELDS) {
|
||||
deletePath(clone, path);
|
||||
}
|
||||
if (Array.isArray(clone.opdsCatalogs)) {
|
||||
clone.opdsCatalogs = clone.opdsCatalogs.map((catalog) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { username: _username, password: _password, ...rest } = catalog;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/** Recursively merge `source` onto `target`; objects merge, scalars/arrays replace. */
|
||||
const deepMerge = (
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>,
|
||||
): Record<string, unknown> => {
|
||||
const out: Record<string, unknown> = { ...target };
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
const existing = out[key];
|
||||
if (isPlainObject(existing) && isPlainObject(value)) {
|
||||
out[key] = deepMerge(existing, value);
|
||||
} else {
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge a restored settings snapshot onto the current device settings.
|
||||
* Blacklisted fields are absent from the snapshot, so the current
|
||||
* device's values for them are preserved. Neither input is mutated.
|
||||
*/
|
||||
export function mergeRestoredSettings(
|
||||
current: SystemSettings,
|
||||
backup: Partial<SystemSettings>,
|
||||
): SystemSettings {
|
||||
return deepMerge(
|
||||
current as unknown as Record<string, unknown>,
|
||||
backup as unknown as Record<string, unknown>,
|
||||
) as unknown as SystemSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two BookConfigs: uses the config with higher reading progress as base,
|
||||
* then merges booknotes from both (deduplicating by id, latest updatedAt wins).
|
||||
@@ -54,6 +190,45 @@ export function mergeBookMetadata(current: Book, backup: Book): Book {
|
||||
return base;
|
||||
}
|
||||
|
||||
/** A book restored from a backup whose local copy had been soft-deleted. */
|
||||
export interface RevivedBook {
|
||||
/** The live library record, mutated in place by `reviveRestoredBooks`. */
|
||||
book: Book;
|
||||
/** The book's metadata as stored in the backup. */
|
||||
backup: Book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix up books revived from a backup — present (not deleted) in the backup
|
||||
* but soft-deleted in the current library — see issue #4098.
|
||||
*
|
||||
* Their files were just re-extracted, so `downloadedAt` / `coverDownloadedAt`
|
||||
* are taken from the backup record (the local deletion had cleared them).
|
||||
*
|
||||
* `updatedAt` is bumped so the restore out-ranks the cloud's deletion
|
||||
* tombstone in the next sync's last-writer-wins merge. A single uniform
|
||||
* offset is applied to every revived book, so their relative `updatedAt`
|
||||
* order — and thus the library's "Updated" sort — is preserved exactly.
|
||||
* `syncedAt` is cleared so the next push re-uploads them and corrects the
|
||||
* cloud rows. Mutates the `book` of each entry in place.
|
||||
*/
|
||||
export function reviveRestoredBooks(revived: RevivedBook[], now: number = Date.now()): void {
|
||||
if (revived.length === 0) return;
|
||||
let maxUpdatedAt = 0;
|
||||
for (const { book } of revived) {
|
||||
if (book.updatedAt > maxUpdatedAt) maxUpdatedAt = book.updatedAt;
|
||||
}
|
||||
// offset >= 1 guarantees every book out-ranks its (un-bumped) cloud copy
|
||||
// while a single shared offset keeps their relative order intact.
|
||||
const offset = Math.max(1, now - maxUpdatedAt);
|
||||
for (const { book, backup } of revived) {
|
||||
book.updatedAt += offset;
|
||||
book.syncedAt = null;
|
||||
book.downloadedAt = backup.downloadedAt ?? book.downloadedAt ?? now;
|
||||
book.coverDownloadedAt = backup.coverDownloadedAt ?? book.coverDownloadedAt ?? now;
|
||||
}
|
||||
}
|
||||
|
||||
/** Library metadata files to skip from the directory scan. */
|
||||
const LIBRARY_META_FILES = new Set([
|
||||
'library.json',
|
||||
@@ -76,6 +251,7 @@ type ProgressCallback = (current: number, total: number, filename: string) => vo
|
||||
async function addBackupEntriesToZip(
|
||||
writer: ZipWriter<unknown>,
|
||||
appService: AppService,
|
||||
options: BackupOptions,
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<void> {
|
||||
const { Uint8ArrayReader } = await import('@zip.js/zip.js');
|
||||
@@ -87,6 +263,17 @@ async function addBackupEntriesToZip(
|
||||
const libraryJson = new TextEncoder().encode(JSON.stringify(libraryBooks, null, 2));
|
||||
await writer.add(getLibraryFilename(), new Uint8ArrayReader(libraryJson));
|
||||
|
||||
// Add the global settings snapshot, sanitized of device-specific and
|
||||
// (unless opted in) credential fields.
|
||||
try {
|
||||
const settings = await appService.loadSettings();
|
||||
const sanitized = sanitizeSettingsForBackup(settings, options);
|
||||
const settingsJson = new TextEncoder().encode(JSON.stringify(sanitized, null, 2));
|
||||
await writer.add(SETTINGS_BACKUP_FILENAME, new Uint8ArrayReader(settingsJson));
|
||||
} catch (error) {
|
||||
console.warn('Skipping settings backup:', error);
|
||||
}
|
||||
|
||||
// Add all book files, skipping library metadata files
|
||||
const booksDir = await appService.resolveFilePath('', 'Books');
|
||||
const files = await appService.readDirectory(booksDir, 'None');
|
||||
@@ -118,6 +305,7 @@ const ZIP_WRITE_CONFIG: Partial<Configuration> = {
|
||||
*/
|
||||
export async function createBackupZip(
|
||||
appService: AppService,
|
||||
options: BackupOptions = {},
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<ArrayBuffer> {
|
||||
await configureZip(ZIP_WRITE_CONFIG);
|
||||
@@ -125,7 +313,7 @@ export async function createBackupZip(
|
||||
|
||||
const blobWriter = new BlobWriter('application/zip');
|
||||
const writer = new ZipWriter(blobWriter);
|
||||
await addBackupEntriesToZip(writer, appService, onProgress);
|
||||
await addBackupEntriesToZip(writer, appService, options, onProgress);
|
||||
await writer.close();
|
||||
const blob = await blobWriter.getData();
|
||||
return await blob.arrayBuffer();
|
||||
@@ -139,6 +327,7 @@ export async function createBackupZip(
|
||||
export async function createBackupZipToFile(
|
||||
appService: AppService,
|
||||
filePath: string,
|
||||
options: BackupOptions = {},
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<void> {
|
||||
await configureZip(ZIP_WRITE_CONFIG);
|
||||
@@ -151,7 +340,7 @@ export async function createBackupZipToFile(
|
||||
const writePromise = writeFile(filePath, readable);
|
||||
|
||||
const writer = new ZipWriter(writable);
|
||||
await addBackupEntriesToZip(writer, appService, onProgress);
|
||||
await addBackupEntriesToZip(writer, appService, options, onProgress);
|
||||
await writer.close();
|
||||
await writePromise;
|
||||
}
|
||||
@@ -170,12 +359,13 @@ export function validateBackupStructure(entryNames: string[]): boolean {
|
||||
* - Merge book config files (keep higher progress, merge notes)
|
||||
* - Add new books not present in current library
|
||||
* - Import orphan hash directories not listed in library.json
|
||||
* - Restore global settings (settings.json), deep-merged onto current
|
||||
*/
|
||||
export async function restoreFromBackupZip(
|
||||
appService: AppService,
|
||||
zipBlob: Blob,
|
||||
onProgress?: (current: number, total: number, filename: string) => void,
|
||||
): Promise<{ booksAdded: number; booksUpdated: number }> {
|
||||
): Promise<{ booksAdded: number; booksUpdated: number; settingsRestored: boolean }> {
|
||||
await configureZip();
|
||||
const { BlobReader, ZipReader, Uint8ArrayWriter } = await import('@zip.js/zip.js');
|
||||
|
||||
@@ -223,6 +413,7 @@ export async function restoreFromBackupZip(
|
||||
|
||||
let booksAdded = 0;
|
||||
let booksUpdated = 0;
|
||||
const revivedBooks: RevivedBook[] = [];
|
||||
const total = backupBooks.length + orphanHashes.size;
|
||||
|
||||
for (let i = 0; i < backupBooks.length; i++) {
|
||||
@@ -259,8 +450,12 @@ export async function restoreFromBackupZip(
|
||||
}
|
||||
}
|
||||
|
||||
// Merge book metadata (timestamps, deletedAt reconciliation)
|
||||
// Merge book metadata (timestamps, deletedAt reconciliation). A book
|
||||
// deleted locally but present in the backup is "revived" — collect it
|
||||
// so its download state and updatedAt can be fixed up after the loop.
|
||||
const wasRevived = !!existingBook.deletedAt && !backupBook.deletedAt;
|
||||
Object.assign(existingBook, mergeBookMetadata(existingBook, backupBook));
|
||||
if (wasRevived) revivedBooks.push({ book: existingBook, backup: backupBook });
|
||||
booksUpdated++;
|
||||
} else {
|
||||
// Add new book: extract all files
|
||||
@@ -313,12 +508,33 @@ export async function restoreFromBackupZip(
|
||||
}
|
||||
}
|
||||
|
||||
// Make revived books out-rank the cloud's deletion tombstone in the
|
||||
// next sync, without disturbing the library's "Updated" sort order.
|
||||
reviveRestoredBooks(revivedBooks);
|
||||
|
||||
// Save merged library
|
||||
await appService.saveLibraryBooks(currentBooks);
|
||||
|
||||
// Restore global settings if the backup carries them. Blacklisted
|
||||
// fields are absent from the snapshot, so the current device keeps
|
||||
// its own values for those after the deep merge.
|
||||
let settingsRestored = false;
|
||||
const settingsEntry = fileEntries.find((e) => e.filename === SETTINGS_BACKUP_FILENAME);
|
||||
if (settingsEntry) {
|
||||
try {
|
||||
const data = await settingsEntry.getData!(new Uint8ArrayWriter());
|
||||
const backupSettings: Partial<SystemSettings> = JSON.parse(new TextDecoder().decode(data));
|
||||
const currentSettings = await appService.loadSettings();
|
||||
await appService.saveSettings(mergeRestoredSettings(currentSettings, backupSettings));
|
||||
settingsRestored = true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore settings from backup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await reader.close();
|
||||
|
||||
return { booksAdded, booksUpdated };
|
||||
return { booksAdded, booksUpdated, settingsRestored };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,6 +545,7 @@ export async function restoreFromBackupZip(
|
||||
export async function saveBackupFile(
|
||||
appService: AppService,
|
||||
filename: string,
|
||||
options: BackupOptions = {},
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<boolean> {
|
||||
if (isTauriAppPlatform()) {
|
||||
@@ -340,11 +557,11 @@ export async function saveBackupFile(
|
||||
filters: [{ name: ext.toUpperCase(), extensions: [ext] }],
|
||||
});
|
||||
if (!filePath) return false;
|
||||
await createBackupZipToFile(appService, filePath, onProgress);
|
||||
await createBackupZipToFile(appService, filePath, options, onProgress);
|
||||
return true;
|
||||
} else {
|
||||
// Web: build zip in memory then save
|
||||
const zipData = await createBackupZip(appService, onProgress);
|
||||
const zipData = await createBackupZip(appService, options, onProgress);
|
||||
let filePath: string | undefined;
|
||||
return appService.saveFile(filename, zipData, { filePath, mimeType: 'application/zip' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user