diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 0856e98b..b09e8793 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1778,5 +1778,6 @@ "No book content or reading data": "بدون محتوى كتب أو بيانات قراءة", "Share anonymous data": "مشاركة بيانات مجهولة", "Not now": "ليس الآن", - "You can change this anytime in Settings.": "يمكنك تغيير هذا في أي وقت من الإعدادات." + "You can change this anytime in Settings.": "يمكنك تغيير هذا في أي وقت من الإعدادات.", + "Then by...": "ثم حسب..." } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 10d783b7..cc8ba4e8 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "কোনো বইয়ের বিষয়বস্তু বা পড়ার ডেটা নেই", "Share anonymous data": "বেনামি ডেটা শেয়ার করুন", "Not now": "এখন নয়", - "You can change this anytime in Settings.": "আপনি যেকোনো সময় সেটিংসে এটি পরিবর্তন করতে পারেন।" + "You can change this anytime in Settings.": "আপনি যেকোনো সময় সেটিংসে এটি পরিবর্তন করতে পারেন।", + "Then by...": "তারপর..." } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 02f1bd05..c80d0695 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "དཔེ་ཆའི་ནང་དོན་ནམ་ཀློག་པའི་གནས་ཚུལ་མེད།", "Share anonymous data": "མིང་མེད་གནས་ཚུལ་མཉམ་སྤྱོད།", "Not now": "ད་ལྟ་མིན།", - "You can change this anytime in Settings.": "ཁྱེད་ཀྱིས་འདི་སྒྲིག་འགོད་ནང་དུས་ནམ་ཡང་བཟོ་བཅོས་གནང་ཆོག།" + "You can change this anytime in Settings.": "ཁྱེད་ཀྱིས་འདི་སྒྲིག་འགོད་ནང་དུས་ནམ་ཡང་བཟོ་བཅོས་གནང་ཆོག།", + "Then by...": "དེ་ནས..." } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 99e4e98b..370a0bc0 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Keine Buchinhalte oder Lesedaten", "Share anonymous data": "Anonyme Daten teilen", "Not now": "Nicht jetzt", - "You can change this anytime in Settings.": "Du kannst dies jederzeit in den Einstellungen ändern." + "You can change this anytime in Settings.": "Du kannst dies jederzeit in den Einstellungen ändern.", + "Then by...": "Dann nach..." } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index aa9de194..dcdad4ac 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Κανένα περιεχόμενο βιβλίου ή δεδομένα ανάγνωσης", "Share anonymous data": "Κοινοποίηση ανώνυμων δεδομένων", "Not now": "Όχι τώρα", - "You can change this anytime in Settings.": "Μπορείτε να το αλλάξετε ανά πάσα στιγμή από τις Ρυθμίσεις." + "You can change this anytime in Settings.": "Μπορείτε να το αλλάξετε ανά πάσα στιγμή από τις Ρυθμίσεις.", + "Then by...": "Στη συνέχεια κατά..." } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 41dd88e7..78b11d87 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Sin contenido de libros ni datos de lectura", "Share anonymous data": "Compartir datos anónimos", "Not now": "Ahora no", - "You can change this anytime in Settings.": "Puedes cambiar esto en cualquier momento en Ajustes." + "You can change this anytime in Settings.": "Puedes cambiar esto en cualquier momento en Ajustes.", + "Then by...": "Después por..." } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index e4b01ac8..35af40f4 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "بدون محتوای کتاب یا داده‌های مطالعه", "Share anonymous data": "اشتراک داده ناشناس", "Not now": "اکنون نه", - "You can change this anytime in Settings.": "می‌توانید این تنظیم را هر زمان از تنظیمات تغییر دهید." + "You can change this anytime in Settings.": "می‌توانید این تنظیم را هر زمان از تنظیمات تغییر دهید.", + "Then by...": "سپس بر اساس..." } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 3b1751a9..c5738954 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Aucun contenu de livre ni donnée de lecture", "Share anonymous data": "Partager les données anonymes", "Not now": "Pas maintenant", - "You can change this anytime in Settings.": "Vous pouvez modifier ce choix à tout moment dans les Paramètres." + "You can change this anytime in Settings.": "Vous pouvez modifier ce choix à tout moment dans les Paramètres.", + "Then by...": "Puis par..." } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 0dbd13fc..eeb3841d 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "ללא תוכן ספרים או נתוני קריאה", "Share anonymous data": "שתף נתונים אנונימיים", "Not now": "לא עכשיו", - "You can change this anytime in Settings.": "אפשר לשנות זאת בכל עת בהגדרות." + "You can change this anytime in Settings.": "אפשר לשנות זאת בכל עת בהגדרות.", + "Then by...": "ואז לפי..." } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 989cb2b4..378f6645 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "कोई पुस्तक सामग्री या पठन डेटा नहीं", "Share anonymous data": "गुमनाम डेटा साझा करें", "Not now": "अभी नहीं", - "You can change this anytime in Settings.": "आप इसे कभी भी सेटिंग्स में बदल सकते हैं।" + "You can change this anytime in Settings.": "आप इसे कभी भी सेटिंग्स में बदल सकते हैं।", + "Then by...": "फिर इसके अनुसार..." } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index cb914756..1307d8f5 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Nincs könyvtartalom vagy olvasási adat", "Share anonymous data": "Anonim adatok megosztása", "Not now": "Most nem", - "You can change this anytime in Settings.": "Ezt bármikor megváltoztathatod a Beállításokban." + "You can change this anytime in Settings.": "Ezt bármikor megváltoztathatod a Beállításokban.", + "Then by...": "Másodlagosan..." } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 0272b17f..4da8ab82 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "Tidak ada konten buku atau data baca", "Share anonymous data": "Bagikan data anonim", "Not now": "Nanti saja", - "You can change this anytime in Settings.": "Anda dapat mengubahnya kapan saja di Pengaturan." + "You can change this anytime in Settings.": "Anda dapat mengubahnya kapan saja di Pengaturan.", + "Then by...": "Lalu berdasarkan..." } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 1d31ddf4..4c9ff896 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Nessun contenuto dei libri o dato di lettura", "Share anonymous data": "Condividi dati anonimi", "Not now": "Non ora", - "You can change this anytime in Settings.": "Puoi modificarlo in qualsiasi momento dalle Impostazioni." + "You can change this anytime in Settings.": "Puoi modificarlo in qualsiasi momento dalle Impostazioni.", + "Then by...": "Poi per..." } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 9603760e..8f079790 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "書籍の内容や読書データは含まれません", "Share anonymous data": "匿名データを共有", "Not now": "後で", - "You can change this anytime in Settings.": "この設定はいつでも設定画面から変更できます。" + "You can change this anytime in Settings.": "この設定はいつでも設定画面から変更できます。", + "Then by...": "次に並び替え..." } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 6be7119f..9bdf1791 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "도서 내용이나 독서 데이터 없음", "Share anonymous data": "익명 데이터 공유", "Not now": "나중에", - "You can change this anytime in Settings.": "설정에서 언제든지 변경할 수 있습니다." + "You can change this anytime in Settings.": "설정에서 언제든지 변경할 수 있습니다.", + "Then by...": "다음 기준..." } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 451bdff7..fd60c4e8 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "Tiada kandungan buku atau data bacaan", "Share anonymous data": "Kongsi data tanpa nama", "Not now": "Bukan sekarang", - "You can change this anytime in Settings.": "Anda boleh menukar ini pada bila-bila masa dalam Tetapan." + "You can change this anytime in Settings.": "Anda boleh menukar ini pada bila-bila masa dalam Tetapan.", + "Then by...": "Kemudian mengikut..." } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index de08f8c0..3622bab9 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Geen boekinhoud of leesgegevens", "Share anonymous data": "Anonieme gegevens delen", "Not now": "Niet nu", - "You can change this anytime in Settings.": "Je kunt dit op elk moment wijzigen in Instellingen." + "You can change this anytime in Settings.": "Je kunt dit op elk moment wijzigen in Instellingen.", + "Then by...": "Vervolgens op..." } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 76272613..69b77bcc 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1716,5 +1716,6 @@ "No book content or reading data": "Bez treści książek i danych o czytaniu", "Share anonymous data": "Udostępnij anonimowe dane", "Not now": "Nie teraz", - "You can change this anytime in Settings.": "Możesz to zmienić w dowolnej chwili w Ustawieniach." + "You can change this anytime in Settings.": "Możesz to zmienić w dowolnej chwili w Ustawieniach.", + "Then by...": "Następnie..." } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 44b8279f..9545c6da 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Sem conteúdo de livros ou dados de leitura", "Share anonymous data": "Compartilhar dados anônimos", "Not now": "Agora não", - "You can change this anytime in Settings.": "Você pode alterar isso a qualquer momento em Configurações." + "You can change this anytime in Settings.": "Você pode alterar isso a qualquer momento em Configurações.", + "Then by...": "Depois por..." } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 20c89a09..406a0e5f 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Sem conteúdo de livros ou dados de leitura", "Share anonymous data": "Partilhar dados anónimos", "Not now": "Agora não", - "You can change this anytime in Settings.": "Pode alterar isto a qualquer momento nas Definições." + "You can change this anytime in Settings.": "Pode alterar isto a qualquer momento nas Definições.", + "Then by...": "Depois por..." } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index ddeffc04..f0b0c040 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1685,5 +1685,6 @@ "No book content or reading data": "Fără conținut din cărți sau date despre citire", "Share anonymous data": "Partajează date anonime", "Not now": "Nu acum", - "You can change this anytime in Settings.": "Poți modifica această opțiune oricând din Setări." + "You can change this anytime in Settings.": "Poți modifica această opțiune oricând din Setări.", + "Then by...": "Apoi după..." } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index dbb62e82..99970bf1 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1716,5 +1716,6 @@ "No book content or reading data": "Без содержимого книг и данных о чтении", "Share anonymous data": "Поделиться анонимными данными", "Not now": "Не сейчас", - "You can change this anytime in Settings.": "Это можно изменить в любое время в Настройках." + "You can change this anytime in Settings.": "Это можно изменить в любое время в Настройках.", + "Then by...": "Затем по..." } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 20dd4bec..b25094c6 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "පොත් අන්තර්ගතය හෝ කියවීමේ දත්ත නැත", "Share anonymous data": "අනාමික දත්ත බෙදාගන්න", "Not now": "දැන් නොවේ", - "You can change this anytime in Settings.": "ඔබට මෙය ඕනෑම විටක සැකසුම් තුළ වෙනස් කළ හැකිය." + "You can change this anytime in Settings.": "ඔබට මෙය ඕනෑම විටක සැකසුම් තුළ වෙනස් කළ හැකිය.", + "Then by...": "ඉන්පසු..." } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 1c8b9e63..a8791665 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1716,5 +1716,6 @@ "No book content or reading data": "Brez vsebine knjig ali bralnih podatkov", "Share anonymous data": "Deli anonimne podatke", "Not now": "Ne zdaj", - "You can change this anytime in Settings.": "To lahko kadar koli spremenite v Nastavitvah." + "You can change this anytime in Settings.": "To lahko kadar koli spremenite v Nastavitvah.", + "Then by...": "Nato po..." } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index b3cc34ab..c20b74cc 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Inget bokinnehåll eller läsdata", "Share anonymous data": "Dela anonyma data", "Not now": "Inte nu", - "You can change this anytime in Settings.": "Du kan ändra detta när som helst i Inställningar." + "You can change this anytime in Settings.": "Du kan ändra detta när som helst i Inställningar.", + "Then by...": "Sedan efter..." } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index f5a000b5..7e2b5a8a 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "புத்தக உள்ளடக்கம் அல்லது வாசிப்புத் தரவு இல்லை", "Share anonymous data": "அநாமதேய தரவைப் பகிர்", "Not now": "இப்போது வேண்டாம்", - "You can change this anytime in Settings.": "அமைப்புகளில் எப்போது வேண்டுமானாலும் இதை மாற்றலாம்." + "You can change this anytime in Settings.": "அமைப்புகளில் எப்போது வேண்டுமானாலும் இதை மாற்றலாம்.", + "Then by...": "பிறகு..." } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index c3eaaf5c..7c52d226 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "ไม่มีเนื้อหาหนังสือหรือข้อมูลการอ่าน", "Share anonymous data": "แชร์ข้อมูลแบบไม่ระบุตัวตน", "Not now": "ไม่ใช่ตอนนี้", - "You can change this anytime in Settings.": "คุณสามารถเปลี่ยนแปลงสิ่งนี้ได้ทุกเมื่อในการตั้งค่า" + "You can change this anytime in Settings.": "คุณสามารถเปลี่ยนแปลงสิ่งนี้ได้ทุกเมื่อในการตั้งค่า", + "Then by...": "จากนั้น..." } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index da05e5eb..b5d2c305 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Kitap içeriği veya okuma verisi yok", "Share anonymous data": "Anonim veri paylaş", "Not now": "Şimdi değil", - "You can change this anytime in Settings.": "Bunu istediğiniz zaman Ayarlar'dan değiştirebilirsiniz." + "You can change this anytime in Settings.": "Bunu istediğiniz zaman Ayarlar'dan değiştirebilirsiniz.", + "Then by...": "Sonra..." } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index ded08501..906a175d 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1716,5 +1716,6 @@ "No book content or reading data": "Жодного вмісту книг чи даних читання", "Share anonymous data": "Поділитися анонімними даними", "Not now": "Не зараз", - "You can change this anytime in Settings.": "Ви можете змінити це будь-коли в Налаштуваннях." + "You can change this anytime in Settings.": "Ви можете змінити це будь-коли в Налаштуваннях.", + "Then by...": "Потім за..." } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index bdbaec51..8a2bb620 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -1654,5 +1654,6 @@ "No book content or reading data": "Kitob mazmuni yoki oʻqish maʼlumotlari yoʻq", "Share anonymous data": "Anonim maʼlumotlarni ulashish", "Not now": "Hozir emas", - "You can change this anytime in Settings.": "Buni xohlagan vaqtda Sozlamalarda oʻzgartirishingiz mumkin." + "You can change this anytime in Settings.": "Buni xohlagan vaqtda Sozlamalarda oʻzgartirishingiz mumkin.", + "Then by...": "Keyin..." } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index c0f8893d..9c433fa9 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "Không có nội dung sách hoặc dữ liệu đọc", "Share anonymous data": "Chia sẻ dữ liệu ẩn danh", "Not now": "Để sau", - "You can change this anytime in Settings.": "Bạn có thể thay đổi tùy chọn này bất cứ lúc nào trong Cài đặt." + "You can change this anytime in Settings.": "Bạn có thể thay đổi tùy chọn này bất cứ lúc nào trong Cài đặt.", + "Then by...": "Sau đó theo..." } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index d5047f96..1f942627 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "不包含书籍内容或阅读数据", "Share anonymous data": "分享匿名数据", "Not now": "暂不", - "You can change this anytime in Settings.": "您可以随时在“设置”中更改此选项。" + "You can change this anytime in Settings.": "您可以随时在“设置”中更改此选项。", + "Then by...": "其次按..." } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 104fb6ee..5c315703 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1623,5 +1623,6 @@ "No book content or reading data": "不包含書籍內容或閱讀資料", "Share anonymous data": "分享匿名資料", "Not now": "暫不", - "You can change this anytime in Settings.": "您可以隨時在「設定」中變更此項目。" + "You can change this anytime in Settings.": "您可以隨時在「設定」中變更此項目。", + "Then by...": "其次按..." } diff --git a/apps/readest-app/src/__tests__/utils/library-utils.test.ts b/apps/readest-app/src/__tests__/utils/library-utils.test.ts index 6d09799a..976e7796 100644 --- a/apps/readest-app/src/__tests__/utils/library-utils.test.ts +++ b/apps/readest-app/src/__tests__/utils/library-utils.test.ts @@ -7,7 +7,10 @@ import { getGroupSortValue, createBookSorter, ensureLibrarySortByType, + ensureLibrarySecondarySortByType, ensureLibraryGroupByType, + resolveEffectivePrimarySort, + resolveEffectiveSecondarySort, findGroupById, getGroupDisplayName, expandBookshelfSelection, @@ -1114,3 +1117,179 @@ describe('buildGroupNameUpdatedAt', () => { expect(groups.map((g) => g.name)).toEqual(['Newest', 'Newer', 'Old']); }); }); + +describe('secondary sort - createBookSorter tiebreaker', () => { + it('uses secondary key as tiebreaker when primary keys are equal', () => { + const books = [ + createMockBook({ hash: 'a', title: 'Zebra', author: 'Same Author' }), + createMockBook({ hash: 'b', title: 'Apple', author: 'Same Author' }), + ]; + const sorter = createBookSorter(LibrarySortByType.Author, 'en', LibrarySortByType.Title); + const sorted = [...books].sort(sorter); + expect(sorted[0]!.title).toBe('Apple'); + expect(sorted[1]!.title).toBe('Zebra'); + }); + + it('falls back to default when primary differs (secondary not used)', () => { + const books = [ + createMockBook({ hash: 'a', title: 'Zebra', author: 'B Author' }), + createMockBook({ hash: 'b', title: 'Apple', author: 'A Author' }), + ]; + const sorter = createBookSorter(LibrarySortByType.Author, 'en', LibrarySortByType.Title); + const sorted = [...books].sort(sorter); + expect(sorted[0]!.author).toBe('A Author'); + expect(sorted[1]!.author).toBe('B Author'); + }); + + it('ignores secondary when set to none', () => { + const books = [ + createMockBook({ hash: 'a', title: 'Zebra', author: 'Same' }), + createMockBook({ hash: 'b', title: 'Apple', author: 'Same' }), + ]; + const sorter = createBookSorter(LibrarySortByType.Author, 'en', 'none'); + const sorted = [...books].sort(sorter); + // Stable: both equal, original order preserved + expect(sorted[0]!.hash).toBe('a'); + expect(sorted[1]!.hash).toBe('b'); + }); +}); + +describe('secondary sort - createWithinGroupSorter (drilled-in group)', () => { + it('inside an author group, sorts by series index when secondary is series', () => { + const books = [ + createMockBook({ + hash: '1', + title: 'Zebra Book', + metadata: { series: 'Series A', seriesIndex: 3 }, + }), + createMockBook({ + hash: '2', + title: 'Apple Book', + metadata: { series: 'Series A', seriesIndex: 1 }, + }), + createMockBook({ + hash: '3', + title: 'Mango Book', + metadata: { series: 'Series A', seriesIndex: 2 }, + }), + ]; + const sorter = createWithinGroupSorter( + LibraryGroupByType.Author, + LibrarySortByType.Title, + 'en', + true, + LibrarySortByType.Series, + ); + const sorted = [...books].sort(sorter); + expect(sorted.map((b) => b.metadata?.seriesIndex)).toEqual([1, 2, 3]); + }); + + it('falls back to primary when secondary ties (same series index)', () => { + const books = [ + createMockBook({ + hash: '1', + title: 'Zebra', + metadata: { seriesIndex: 1 }, + }), + createMockBook({ + hash: '2', + title: 'Apple', + metadata: { seriesIndex: 1 }, + }), + ]; + const sorter = createWithinGroupSorter( + LibraryGroupByType.Author, + LibrarySortByType.Title, + 'en', + true, + LibrarySortByType.Series, + ); + const sorted = [...books].sort(sorter); + expect(sorted[0]!.title).toBe('Apple'); + expect(sorted[1]!.title).toBe('Zebra'); + }); + + it('without secondary, keeps existing primary behavior in author group', () => { + const books = [ + createMockBook({ hash: '1', title: 'Zebra' }), + createMockBook({ hash: '2', title: 'Apple' }), + ]; + const sorter = createWithinGroupSorter( + LibraryGroupByType.Author, + LibrarySortByType.Title, + 'en', + true, + 'none', + ); + const sorted = [...books].sort(sorter); + expect(sorted[0]!.title).toBe('Apple'); + expect(sorted[1]!.title).toBe('Zebra'); + }); +}); + +describe('resolveEffectiveSecondarySort smart defaults', () => { + it('returns explicit secondary when not none', () => { + expect(resolveEffectiveSecondarySort(LibrarySortByType.Title, LibraryGroupByType.Author)).toBe( + LibrarySortByType.Title, + ); + }); + + it('defaults to series when groupBy=Author and stored=none', () => { + expect(resolveEffectiveSecondarySort('none', LibraryGroupByType.Author)).toBe( + LibrarySortByType.Series, + ); + }); + + it('stays none for groupBy=Series (series-index handles ordering)', () => { + expect(resolveEffectiveSecondarySort('none', LibraryGroupByType.Series)).toBe('none'); + }); + + it('stays none for groupBy=None/Group', () => { + expect(resolveEffectiveSecondarySort('none', LibraryGroupByType.None)).toBe('none'); + expect(resolveEffectiveSecondarySort('none', LibraryGroupByType.Group)).toBe('none'); + }); +}); + +describe('resolveEffectivePrimarySort smart defaults', () => { + it('returns stored when isAuto=false, regardless of groupBy', () => { + expect( + resolveEffectivePrimarySort(LibrarySortByType.Updated, LibraryGroupByType.Series, false), + ).toBe(LibrarySortByType.Updated); + expect( + resolveEffectivePrimarySort(LibrarySortByType.Title, LibraryGroupByType.Author, false), + ).toBe(LibrarySortByType.Title); + }); + + it('defaults to Series when isAuto=true and groupBy=Series', () => { + expect( + resolveEffectivePrimarySort(LibrarySortByType.Updated, LibraryGroupByType.Series, true), + ).toBe(LibrarySortByType.Series); + }); + + it('returns stored for non-series groupBy when isAuto=true', () => { + expect( + resolveEffectivePrimarySort(LibrarySortByType.Updated, LibraryGroupByType.Author, true), + ).toBe(LibrarySortByType.Updated); + expect( + resolveEffectivePrimarySort(LibrarySortByType.Updated, LibraryGroupByType.Group, true), + ).toBe(LibrarySortByType.Updated); + expect( + resolveEffectivePrimarySort(LibrarySortByType.Updated, LibraryGroupByType.None, true), + ).toBe(LibrarySortByType.Updated); + }); +}); + +describe('ensureLibrarySecondarySortByType', () => { + it('accepts none', () => { + expect(ensureLibrarySecondarySortByType('none', 'none')).toBe('none'); + }); + it('accepts any valid LibrarySortByType', () => { + expect(ensureLibrarySecondarySortByType('series', 'none')).toBe(LibrarySortByType.Series); + }); + it('falls back when invalid', () => { + expect(ensureLibrarySecondarySortByType('bogus', LibrarySortByType.Title)).toBe( + LibrarySortByType.Title, + ); + expect(ensureLibrarySecondarySortByType(null, 'none')).toBe('none'); + }); +}); diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index 9edbcf8e..87b5347e 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -36,10 +36,13 @@ import { createWithinGroupSorter, ensureLibraryGroupByType, ensureLibrarySortByType, + ensureLibrarySecondarySortByType, expandBookshelfSelection, getBookSortValue, getGroupSortValue, compareSortValues, + resolveEffectivePrimarySort, + resolveEffectiveSecondarySort, } from '../utils/libraryUtils'; import { eventDispatcher } from '@/utils/event'; @@ -155,9 +158,16 @@ const Bookshelf: React.FC = ({ const groupId = searchParams?.get('group') || ''; const queryTerm = searchParams?.get('q') || null; const viewMode = searchParams?.get('view') || settings.libraryViewMode; - const sortBy = ensureLibrarySortByType(searchParams?.get('sort'), settings.librarySortBy); + const storedSortBy = ensureLibrarySortByType(searchParams?.get('sort'), settings.librarySortBy); const sortOrder = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'); const groupBy = ensureLibraryGroupByType(searchParams?.get('groupBy'), settings.libraryGroupBy); + const sortByAuto = settings.librarySortByAuto ?? true; + const sortBy = resolveEffectivePrimarySort(storedSortBy, groupBy, sortByAuto); + const sortBy2Raw = ensureLibrarySecondarySortByType( + searchParams?.get('sort2'), + settings.librarySortBy2 ?? 'none', + ); + const sortBy2 = resolveEffectiveSecondarySort(sortBy2Raw, groupBy); const coverFit = searchParams?.get('cover') || settings.libraryCoverFit; const [loading, setLoading] = useState(false); @@ -260,14 +270,20 @@ const Bookshelf: React.FC = ({ // Sort books within each group // For series groups, series index is always ascending; sort direction applies to fallback only const sortAscending = sortOrder === 'asc'; - const withinGroupSorter = createWithinGroupSorter(groupBy, sortBy, uiLanguage, sortAscending); + const withinGroupSorter = createWithinGroupSorter( + groupBy, + sortBy, + uiLanguage, + sortAscending, + sortBy2, + ); groups.forEach((group) => { group.books.sort(withinGroupSorter); }); // Sort ungrouped books - use within-group sorter if we're inside a group // (for series, this ensures books are sorted by series index) - const bookSorter = createBookSorter(sortBy, uiLanguage); + const bookSorter = createBookSorter(sortBy, uiLanguage, sortBy2); if (groupId && groupBy !== LibraryGroupByType.Group && groupBy !== LibraryGroupByType.None) { ungroupedBooks.sort(withinGroupSorter); // When inside a group, books are already sorted correctly — return directly @@ -309,7 +325,7 @@ const Bookshelf: React.FC = ({ }); return allItems; - }, [sortOrder, sortBy, groupBy, groupId, uiLanguage, currentBookshelfItems]); + }, [sortOrder, sortBy, sortBy2, groupBy, groupId, uiLanguage, currentBookshelfItems]); useEffect(() => { if (isImportingBook.current) return; diff --git a/apps/readest-app/src/app/library/components/ViewMenu.tsx b/apps/readest-app/src/app/library/components/ViewMenu.tsx index 63c14f38..d83cbc40 100644 --- a/apps/readest-app/src/app/library/components/ViewMenu.tsx +++ b/apps/readest-app/src/app/library/components/ViewMenu.tsx @@ -7,6 +7,7 @@ import { LibraryCoverFitType, LibraryViewModeType, LibraryGroupByType, + LibrarySecondarySortByType, LibrarySortByType, } from '@/types/settings'; import { saveSysSettings } from '@/helpers/settings'; @@ -33,6 +34,22 @@ const ViewMenu: React.FC = ({ setIsDropdownOpen }) => { const groupBy = settings.libraryGroupBy; const sortBy = settings.librarySortBy; const isAscending = settings.librarySortAscending; + const sortByAuto = settings.librarySortByAuto ?? true; + // Primary smart default: when auto is on, grouping by Series implies Series as + // the primary sort. The stored value is left alone — that way turning auto off + // later restores the user's previous explicit pick. + const primaryEffective: LibrarySortByType = + sortByAuto && groupBy === LibraryGroupByType.Series ? LibrarySortByType.Series : sortBy; + const primaryIsImplicit = sortByAuto && primaryEffective !== sortBy; + const sortBy2: LibrarySecondarySortByType = settings.librarySortBy2 ?? 'none'; + // Smart default: when grouping by Author and the user hasn't picked an explicit + // secondary, Series is implied. Surface this in the menu so the highlighted row + // matches the actual sort behavior. + const secondaryEffective: LibrarySecondarySortByType = + sortBy2 === 'none' && groupBy === LibraryGroupByType.Author + ? LibrarySortByType.Series + : sortBy2; + const secondaryIsImplicit = sortBy2 === 'none' && secondaryEffective !== 'none'; const viewOptions = [ { label: _('List'), value: 'list' }, @@ -61,6 +78,11 @@ const ViewMenu: React.FC = ({ setIsDropdownOpen }) => { { label: _('Date Published'), value: LibrarySortByType.Published }, ]; + const sortBy2Options: { label: string; value: LibrarySecondarySortByType }[] = [ + { label: _('None'), value: 'none' }, + ...sortByOptions, + ]; + const sortingOptions = [ { label: _('Ascending'), value: true }, { label: _('Descending'), value: false }, @@ -108,6 +130,9 @@ const ViewMenu: React.FC = ({ setIsDropdownOpen }) => { const handleSetSortBy = async (value: LibrarySortByType) => { await saveSysSettings(envConfig, 'librarySortBy', value); + // Any explicit primary pick locks in the choice and disables the auto + // smart-default so future groupBy changes don't override the user. + await saveSysSettings(envConfig, 'librarySortByAuto', false); const params = new URLSearchParams(searchParams?.toString()); params.set('sort', value); @@ -122,6 +147,18 @@ const ViewMenu: React.FC = ({ setIsDropdownOpen }) => { navigateToLibrary(router, `${params.toString()}`); }; + const handleSetSortBy2 = async (value: LibrarySecondarySortByType) => { + await saveSysSettings(envConfig, 'librarySortBy2', value); + + const params = new URLSearchParams(searchParams?.toString()); + if (value === 'none') { + params.delete('sort2'); + } else { + params.set('sort2', value); + } + navigateToLibrary(router, `${params.toString()}`); + }; + return ( = ({ setIsDropdownOpen }) => {
    - {sortByOptions.map((option) => ( - handleSetSortBy(option.value as LibrarySortByType)} - transient - /> - ))} + {sortByOptions.map((option) => { + const isImplicit = primaryIsImplicit && option.value === primaryEffective; + const toggled = isImplicit || (!primaryIsImplicit && sortBy === option.value); + return ( + handleSetSortBy(option.value as LibrarySortByType)} + transient + /> + ); + })} {sortingOptions.map((option) => ( = ({ setIsDropdownOpen }) => { ))}
+ + {/* Then by - secondary sort, collapsible */} + + +
    + {sortBy2Options.map((option) => { + const isImplicit = secondaryIsImplicit && option.value === secondaryEffective; + const isExplicit = sortBy2 === option.value; + return ( + handleSetSortBy2(option.value)} + transient + /> + ); + })} +
+
); }; diff --git a/apps/readest-app/src/app/library/utils/libraryUtils.ts b/apps/readest-app/src/app/library/utils/libraryUtils.ts index 538ae12e..94dfa93c 100644 --- a/apps/readest-app/src/app/library/utils/libraryUtils.ts +++ b/apps/readest-app/src/app/library/utils/libraryUtils.ts @@ -1,5 +1,9 @@ import { Book, BooksGroup } from '@/types/book'; -import { LibraryGroupByType, LibrarySortByType } from '@/types/settings'; +import { + LibraryGroupByType, + LibrarySecondarySortByType, + LibrarySortByType, +} from '@/types/settings'; import { formatAuthors, formatTitle } from '@/utils/book'; import { md5Fingerprint } from '@/utils/md5'; @@ -23,6 +27,65 @@ export const ensureLibrarySortByType = ( return fallback; }; +/** + * Safely cast a query parameter to LibrarySecondarySortByType with fallback. + * Accepts any valid primary sort type plus the literal 'none'. + */ +export const ensureLibrarySecondarySortByType = ( + value: string | null | undefined, + fallback: LibrarySecondarySortByType, +): LibrarySecondarySortByType => { + if (value === 'none') return 'none'; + if (value && VALID_SORT_TYPES.includes(value as LibrarySortByType)) { + return value as LibrarySortByType; + } + return fallback; +}; + +/** + * Resolve the *effective* primary sort key, applying smart defaults derived + * from the current `groupBy`. The stored `librarySortBy` is left unchanged; + * this only substitutes a sensible implicit default when the user is still on + * auto, so grouping by Series lands an alphabetical-by-series-name listing + * without any extra clicks. + * + * - !isAuto -> use stored as-is + * - groupBy=Series and isAuto -> Series + * - everything else -> stored + */ +export const resolveEffectivePrimarySort = ( + stored: LibrarySortByType, + groupBy: LibraryGroupByType, + isAuto: boolean, +): LibrarySortByType => { + if (!isAuto) return stored; + if (groupBy === LibraryGroupByType.Series) return LibrarySortByType.Series; + return stored; +}; + +/** + * Resolve the *effective* secondary sort key, applying smart defaults derived + * from the current `groupBy`. The stored secondary stays whatever the user + * picked; this only substitutes 'none' with a sensible implicit default so + * users get useful behavior out of the box (e.g. drilling into an Author + * group lands a series-ordered list without any extra clicks). + * + * - explicit secondary (any non-'none') -> use as-is + * - groupBy=Author and stored='none' -> Series + * - everything else -> 'none' + * + * groupBy=Series doesn't default to anything because `createWithinGroupSorter` + * already orders by `seriesIndex` for series groups. + */ +export const resolveEffectiveSecondarySort = ( + secondary: LibrarySecondarySortByType, + groupBy: LibraryGroupByType, +): LibrarySecondarySortByType => { + if (secondary !== 'none') return secondary; + if (groupBy === LibraryGroupByType.Author) return LibrarySortByType.Series; + return 'none'; +}; + /** * Safely cast a query parameter to LibraryGroupByType with fallback. * Returns the value if valid, otherwise returns the fallback. @@ -115,16 +178,18 @@ export const createBookFilter = (queryTerm: string | null) => (item: Book) => { ); }; -export const createBookSorter = (sortBy: string, uiLanguage: string) => (a: Book, b: Book) => { +const compareBookByKey = (a: Book, b: Book, sortBy: string, uiLanguage: string): number => { switch (sortBy) { - case LibrarySortByType.Title: + case LibrarySortByType.Title: { const aTitle = formatTitle(a.title); const bTitle = formatTitle(b.title); return aTitle.localeCompare(bTitle, uiLanguage || navigator.language); - case LibrarySortByType.Author: + } + case LibrarySortByType.Author: { const aAuthors = formatAuthors(a.author, a?.primaryLanguage || 'en', true); const bAuthors = formatAuthors(b.author, b?.primaryLanguage || 'en', true); return aAuthors.localeCompare(bAuthors, uiLanguage || navigator.language); + } case LibrarySortByType.Updated: return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); case LibrarySortByType.Created: @@ -133,7 +198,7 @@ export const createBookSorter = (sortBy: string, uiLanguage: string) => (a: Book return a.format.localeCompare(b.format, uiLanguage || navigator.language); case LibrarySortByType.Series: return (a.metadata?.seriesIndex || 0) - (b.metadata?.seriesIndex || 0); - case LibrarySortByType.Published: + case LibrarySortByType.Published: { const aPublished = a.metadata?.published || '0001-01-01'; const bPublished = b.metadata?.published || '0001-01-01'; @@ -154,11 +219,25 @@ export const createBookSorter = (sortBy: string, uiLanguage: string) => (a: Book if (isNaN(bDate)) return -1; return aDate - bDate; + } default: return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); } }; +/** + * @param secondarySortBy - Optional tiebreaker key applied when the primary + * comparison returns 0. Pass `'none'` (or omit) to disable. Series-index ties + * on a Series secondary fall through to the underlying primary tie. + */ +export const createBookSorter = + (sortBy: string, uiLanguage: string, secondarySortBy: LibrarySecondarySortByType = 'none') => + (a: Book, b: Book): number => { + const primary = compareBookByKey(a, b, sortBy, uiLanguage); + if (primary !== 0 || secondarySortBy === 'none') return primary; + return compareBookByKey(a, b, secondarySortBy, uiLanguage); + }; + /** * Build a `groupName -> max(book.updatedAt)` map for all groups touched by * the given books. Each book bumps both its direct group and every ancestor @@ -319,10 +398,14 @@ const createAuthorGroups = (books: Book[]): (Book | BooksGroup)[] => { /** * Create a sorter for books within a group. * For series groups: sort by seriesIndex first (always ascending), then by global sort for items without index. - * For author groups: follow global sort setting. - * @param sortAscending - When true (default), sort direction is ascending. Series index is always - * ascending regardless of this flag; the flag only affects the fallback sort for books without - * series index and non-series groupings. + * For other groupings: when a secondary key is supplied, sort by secondary key first (always ascending), + * with the primary global sort as tiebreaker. Without secondary, follow global sort setting. + * @param sortAscending - When true (default), sort direction is ascending. Series index and the + * secondary key are always ascending regardless of this flag; the flag affects the fallback / + * primary tiebreaker only. + * @param secondarySortBy - When non-'none', acts as the *primary* within-group ordering for + * non-series groupings (matches the user's mental model: "group by author, then sort by series" + * should land series order inside each author). */ export const createWithinGroupSorter = ( @@ -330,6 +413,7 @@ export const createWithinGroupSorter = sortBy: LibrarySortByType, uiLanguage: string, sortAscending: boolean = true, + secondarySortBy: LibrarySecondarySortByType = 'none', ) => (a: Book, b: Book): number => { const sortDirection = sortAscending ? 1 : -1; @@ -351,7 +435,14 @@ export const createWithinGroupSorter = return createBookSorter(sortBy, uiLanguage)(a, b) * sortDirection; } - // For author and other groupings, use global sort with direction + // For author and other non-series groupings: when a secondary key is provided, + // use it as the within-group primary order with the global key as tiebreaker. + if (secondarySortBy !== 'none') { + const bySecondary = compareBookByKey(a, b, secondarySortBy, uiLanguage); + if (bySecondary !== 0) return bySecondary; + return createBookSorter(sortBy, uiLanguage)(a, b) * sortDirection; + } + return createBookSorter(sortBy, uiLanguage)(a, b) * sortDirection; }; diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index 9a85425b..96d53399 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -121,6 +121,8 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial = { libraryViewMode: 'grid', librarySortBy: LibrarySortByType.Updated, librarySortAscending: false, + librarySortByAuto: true, + librarySortBy2: 'none', libraryGroupBy: LibraryGroupByType.Group, libraryCoverFit: 'crop', libraryAutoColumns: true, diff --git a/apps/readest-app/src/types/settings.ts b/apps/readest-app/src/types/settings.ts index 06dce0c1..6ee3e635 100644 --- a/apps/readest-app/src/types/settings.ts +++ b/apps/readest-app/src/types/settings.ts @@ -22,6 +22,15 @@ export const LibrarySortByType = { export type LibrarySortByType = (typeof LibrarySortByType)[keyof typeof LibrarySortByType]; +/** + * Secondary sort key. Same options as the primary sort key plus `'none'` which + * disables the secondary sort. When set to `'none'` and a smart default applies + * (e.g. groupBy=Author -> series), the resolver in `libraryUtils` substitutes + * the implicit default at sort time without persisting it. See + * `resolveEffectiveSecondarySort`. + */ +export type LibrarySecondarySortByType = LibrarySortByType | 'none'; + export type LibraryCoverFitType = 'crop' | 'fit'; export const LibraryGroupByType = { @@ -288,6 +297,15 @@ export interface SystemSettings { libraryViewMode: LibraryViewModeType; librarySortBy: LibrarySortByType; librarySortAscending: boolean; + /** + * Whether the primary sort uses a smart default derived from `libraryGroupBy`. + * When `true` and grouping by Series, the effective primary sort becomes + * Series at sort time (the stored `librarySortBy` is left unchanged so users + * who later turn auto off keep their previous explicit pick). Flipped to + * `false` the moment the user picks any primary sort in the menu. + */ + librarySortByAuto: boolean; + librarySortBy2: LibrarySecondarySortByType; libraryGroupBy: LibraryGroupByType; libraryCoverFit: LibraryCoverFitType; libraryAutoColumns: boolean;