forked from akai/readest
This commit is contained in:
@@ -1006,5 +1006,19 @@
|
||||
"Finished": "منتهى",
|
||||
"Unread": "غير مقروء",
|
||||
"Clear Status": "مسح الحالة",
|
||||
"Status": "الحالة"
|
||||
"Status": "الحالة",
|
||||
"Set status for {{count}} book(s)_zero": "تعيين الحالة للكتب",
|
||||
"Set status for {{count}} book(s)_two": "تعيين الحالة للكتابين",
|
||||
"Set status for {{count}} book(s)_few": "تعيين الحالة لـ {{count}} كتب",
|
||||
"Set status for {{count}} book(s)_many": "تعيين الحالة لـ {{count}} كتاباً",
|
||||
"Loading": "جارٍ التحميل...",
|
||||
"Exit Paragraph Mode": "الخروج من وضع الفقرة",
|
||||
"Paragraph Mode": "وضع الفقرة",
|
||||
"Embedding Model": "نموذج التضمين",
|
||||
"{{count}} book(s) synced_zero": "لم تتم مزامنة أي كتب",
|
||||
"{{count}} book(s) synced_one": "تمت مزامنة كتاب واحد",
|
||||
"{{count}} book(s) synced_two": "تمت مزامنة كتابين",
|
||||
"{{count}} book(s) synced_few": "تمت مزامنة {{count}} كتب",
|
||||
"{{count}} book(s) synced_many": "تمت مزامنة {{count}} كتاباً",
|
||||
"{{count}} book(s) synced_other": "تمت مزامنة {{count}} كتاب"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "শেষ হয়েছে",
|
||||
"Unread": "অপঠিত",
|
||||
"Clear Status": "স্থিতি মুছুন",
|
||||
"Status": "অবস্থা"
|
||||
"Status": "অবস্থা",
|
||||
"Loading": "লোড হচ্ছে...",
|
||||
"Exit Paragraph Mode": "অনুচ্ছেদ মোড থেকে প্রস্থান করুন",
|
||||
"Paragraph Mode": "অনুচ্ছেদ মোড",
|
||||
"Embedding Model": "এম্বেডিং মডেল",
|
||||
"{{count}} book(s) synced_one": "{{count}}টি বই সিঙ্ক করা হয়েছে",
|
||||
"{{count}} book(s) synced_other": "{{count}}টি বই সিঙ্ক করা হয়েছে"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "AI Gateway དཔེ་གཞི།",
|
||||
"Actions": "བྱ་འགུལ།",
|
||||
"Navigation": "འགྲུལ་བཞུད།",
|
||||
"Set status for {{count}} book(s)_one": "དེབ་ {{count}} གི་གནས་སྟངས་གཏན་འཁེལ་བྱེད།",
|
||||
"Set status for {{count}} book(s)_other": "དེབ་ {{count}} གི་གནས་སྟངས་གཏན་འཁེལ་བྱེད།",
|
||||
"Mark as Unread": "མ་ཀློག་པར་རྟགས་རྒྱག",
|
||||
"Mark as Finished": "ཚར་བར་རྟགས་རྒྱག",
|
||||
"Finished": "ཚར་སོང་།",
|
||||
"Unread": "མ་ཀློག་པ།",
|
||||
"Clear Status": "གནས་སྟངས་གཙང་མ།",
|
||||
"Status": "གནས་སྟངས།"
|
||||
"Status": "གནས་སྟངས།",
|
||||
"Loading": "ལེན་བཞིན་པ།...",
|
||||
"Exit Paragraph Mode": "དུམ་མཚམས་རྣམ་པ་ནས་ཕྱིར་ཐོན།",
|
||||
"Paragraph Mode": "དུམ་མཚམས་རྣམ་པ།",
|
||||
"Embedding Model": "གནས་སྒྲིག་དཔེ་གཞི།",
|
||||
"{{count}} book(s) synced_other": "དེབ་ {{count}} མཉམ་འགྲིག་བྱས་ཟིན།"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "Beendet",
|
||||
"Unread": "Ungelesen",
|
||||
"Clear Status": "Status löschen",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Loading": "Laden...",
|
||||
"Exit Paragraph Mode": "Absatzmodus verlassen",
|
||||
"Paragraph Mode": "Absatzmodus",
|
||||
"Embedding Model": "Einbettungsmodell",
|
||||
"{{count}} book(s) synced_one": "{{count}} Buch synchronisiert",
|
||||
"{{count}} book(s) synced_other": "{{count}} Bücher synchronisiert"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "Ολοκληρώθηκε",
|
||||
"Unread": "Μη αναγνωσμένο",
|
||||
"Clear Status": "Εκκαθάριση κατάστασης",
|
||||
"Status": "Κατάσταση"
|
||||
"Status": "Κατάσταση",
|
||||
"Loading": "Φόρτωση...",
|
||||
"Exit Paragraph Mode": "Έξοδος από τη λειτουργία παραγράφου",
|
||||
"Paragraph Mode": "Λειτουργία παραγράφου",
|
||||
"Embedding Model": "Μοντέλο ενσωμάτωσης",
|
||||
"{{count}} book(s) synced_one": "{{count}} βιβλίο συγχρονίστηκε",
|
||||
"{{count}} book(s) synced_other": "{{count}} βιβλία συγχρονίστηκαν"
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books",
|
||||
"Set status for {{count}} book(s)_one": "Set status for {{count}} book",
|
||||
"Set status for {{count}} book(s)_other": "Set status for {{count}} books",
|
||||
"Mark as Finished": "Mark as Finished",
|
||||
"Mark as Unread": "Mark as Unread",
|
||||
"Finished": "Finished",
|
||||
"Unread": "Unread",
|
||||
"Clear Status": "Clear Status",
|
||||
"Status": "Status"
|
||||
"{{count}} book(s) synced_one": "{{count}} book synced",
|
||||
"{{count}} book(s) synced_other": "{{count}} books synced"
|
||||
}
|
||||
|
||||
@@ -976,5 +976,13 @@
|
||||
"Finished": "Terminado",
|
||||
"Unread": "Sin leer",
|
||||
"Clear Status": "Borrar estado",
|
||||
"Status": "Estado"
|
||||
"Status": "Estado",
|
||||
"Set status for {{count}} book(s)_many": "Establecer estado para {{count}} libros",
|
||||
"Loading": "Cargando...",
|
||||
"Exit Paragraph Mode": "Salir del modo de párrafo",
|
||||
"Paragraph Mode": "Modo de párrafo",
|
||||
"Embedding Model": "Modelo de incrustación",
|
||||
"{{count}} book(s) synced_one": "{{count}} libro sincronizado",
|
||||
"{{count}} book(s) synced_many": "{{count}} libros sincronizados",
|
||||
"{{count}} book(s) synced_other": "{{count}} libros sincronizados"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "تمامشده",
|
||||
"Unread": "خواندهنشده",
|
||||
"Clear Status": "پاک کردن وضعیت",
|
||||
"Status": "وضعیت"
|
||||
"Status": "وضعیت",
|
||||
"Loading": "در حال بارگذاری...",
|
||||
"Exit Paragraph Mode": "خروج از حالت بند",
|
||||
"Paragraph Mode": "حالت بند",
|
||||
"Embedding Model": "مدل جاسازی",
|
||||
"{{count}} book(s) synced_one": "{{count}} کتاب همگام شد",
|
||||
"{{count}} book(s) synced_other": "{{count}} کتاب همگام شدند"
|
||||
}
|
||||
|
||||
@@ -976,5 +976,13 @@
|
||||
"Finished": "Terminé",
|
||||
"Unread": "Non lu",
|
||||
"Clear Status": "Effacer le statut",
|
||||
"Status": "Statut"
|
||||
"Status": "Statut",
|
||||
"Set status for {{count}} book(s)_many": "Définir le statut pour {{count}} livres",
|
||||
"Loading": "Chargement...",
|
||||
"Exit Paragraph Mode": "Quitter le mode paragraphe",
|
||||
"Paragraph Mode": "Mode paragraphe",
|
||||
"Embedding Model": "Modèle d'incorporation",
|
||||
"{{count}} book(s) synced_one": "{{count}} livre synchronisé",
|
||||
"{{count}} book(s) synced_many": "{{count}} livres synchronisés",
|
||||
"{{count}} book(s) synced_other": "{{count}} livres synchronisés"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "समाप्त",
|
||||
"Unread": "अपठित",
|
||||
"Clear Status": "स्थिति साफ़ करें",
|
||||
"Status": "स्थिति"
|
||||
"Status": "स्थिति",
|
||||
"Loading": "लोड हो रहा है...",
|
||||
"Exit Paragraph Mode": "पैराग्राफ मोड से बाहर निकलें",
|
||||
"Paragraph Mode": "पैराग्राफ मोड",
|
||||
"Embedding Model": "एंबेडिंग मॉडल",
|
||||
"{{count}} book(s) synced_one": "{{count}} पुस्तक सिंक की गई",
|
||||
"{{count}} book(s) synced_other": "{{count}} पुस्तकें सिंक की गईं"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_one": "Atur status untuk {{count}} buku",
|
||||
"Set status for {{count}} book(s)_other": "Atur status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandai sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandai sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Hapus Status",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Loading": "Memuat...",
|
||||
"Exit Paragraph Mode": "Keluar dari Mode Paragraf",
|
||||
"Paragraph Mode": "Mode Paragraf",
|
||||
"Embedding Model": "Model Penyematan",
|
||||
"{{count}} book(s) synced_other": "{{count}} buku telah disinkronkan"
|
||||
}
|
||||
|
||||
@@ -976,5 +976,13 @@
|
||||
"Finished": "Finito",
|
||||
"Unread": "Non letto",
|
||||
"Clear Status": "Cancella stato",
|
||||
"Status": "Stato"
|
||||
"Status": "Stato",
|
||||
"Set status for {{count}} book(s)_many": "Imposta stato per {{count}} libri",
|
||||
"Loading": "Caricamento...",
|
||||
"Exit Paragraph Mode": "Esci dalla modalità paragrafo",
|
||||
"Paragraph Mode": "Modalità paragrafo",
|
||||
"Embedding Model": "Modello di incorporazione",
|
||||
"{{count}} book(s) synced_one": "{{count}} libro sincronizzato",
|
||||
"{{count}} book(s) synced_many": "{{count}} libri sincronizzati",
|
||||
"{{count}} book(s) synced_other": "{{count}} libri sincronizzati"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "AI Gatewayモデル",
|
||||
"Actions": "アクション",
|
||||
"Navigation": "ナビゲーション",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} 冊の本のステータスを設定",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} 冊の本のステータスを設定",
|
||||
"Mark as Unread": "未読にする",
|
||||
"Mark as Finished": "読了にする",
|
||||
"Finished": "読了",
|
||||
"Unread": "未読",
|
||||
"Clear Status": "ステータスをクリア",
|
||||
"Status": "ステータス"
|
||||
"Status": "ステータス",
|
||||
"Loading": "読み込み中",
|
||||
"Exit Paragraph Mode": "段落モードを終了",
|
||||
"Paragraph Mode": "段落モード",
|
||||
"Embedding Model": "埋め込みモデル",
|
||||
"{{count}} book(s) synced_other": "{{count}} 冊の本が同期されました"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "AI 게이트웨이 모델",
|
||||
"Actions": "작업",
|
||||
"Navigation": "탐색",
|
||||
"Set status for {{count}} book(s)_one": "책 {{count}}권 상태 설정",
|
||||
"Set status for {{count}} book(s)_other": "책 {{count}}권 상태 설정",
|
||||
"Mark as Unread": "읽지 않음으로 표시",
|
||||
"Mark as Finished": "완료로 표시",
|
||||
"Finished": "완료",
|
||||
"Unread": "읽지 않음",
|
||||
"Clear Status": "상태 지우기",
|
||||
"Status": "상태"
|
||||
"Status": "상태",
|
||||
"Loading": "로드 중...",
|
||||
"Exit Paragraph Mode": "단락 모드 종료",
|
||||
"Paragraph Mode": "단락 모드",
|
||||
"Embedding Model": "임베딩 모델",
|
||||
"{{count}} book(s) synced_other": "{{count}}권의 책이 동기화되었습니다"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_one": "Tetapkan status untuk {{count}} buku",
|
||||
"Set status for {{count}} book(s)_other": "Tetapkan status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandakan sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandakan sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Kosongkan Status",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Loading": "Memuatkan...",
|
||||
"Exit Paragraph Mode": "Keluar dari Mod Perenggan",
|
||||
"Paragraph Mode": "Mod Perenggan",
|
||||
"Embedding Model": "Model Benaman",
|
||||
"{{count}} book(s) synced_other": "{{count}} buku telah disegerakkan"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "Voltooid",
|
||||
"Unread": "Ongelezen",
|
||||
"Clear Status": "Status wissen",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Loading": "Laden...",
|
||||
"Exit Paragraph Mode": "Alineamodus verlaten",
|
||||
"Paragraph Mode": "Alineamodus",
|
||||
"Embedding Model": "Insluitmodel",
|
||||
"{{count}} book(s) synced_one": "{{count}} boek gesynchroniseerd",
|
||||
"{{count}} book(s) synced_other": "{{count}} boeken gesynchroniseerd"
|
||||
}
|
||||
|
||||
@@ -986,5 +986,15 @@
|
||||
"Finished": "Ukończone",
|
||||
"Unread": "Nieprzeczytane",
|
||||
"Clear Status": "Wyczyść status",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Set status for {{count}} book(s)_few": "Ustaw status dla {{count}} książek",
|
||||
"Set status for {{count}} book(s)_many": "Ustaw status dla {{count}} książek",
|
||||
"Loading": "Ładowanie...",
|
||||
"Exit Paragraph Mode": "Wyjdź z trybu akapitu",
|
||||
"Paragraph Mode": "Tryb akapitu",
|
||||
"Embedding Model": "Model osadzania",
|
||||
"{{count}} book(s) synced_one": "{{count}} książka zsynchronizowana",
|
||||
"{{count}} book(s) synced_few": "{{count}} książki zsynchronizowane",
|
||||
"{{count}} book(s) synced_many": "{{count}} książek zsynchronizowanych",
|
||||
"{{count}} book(s) synced_other": "{{count}} książek zsynchronizowanych"
|
||||
}
|
||||
|
||||
@@ -976,5 +976,13 @@
|
||||
"Finished": "Concluído",
|
||||
"Unread": "Não lido",
|
||||
"Clear Status": "Limpar estado",
|
||||
"Status": "Estado"
|
||||
"Status": "Estado",
|
||||
"Set status for {{count}} book(s)_many": "Definir status para {{count}} livros",
|
||||
"Loading": "Carregando...",
|
||||
"Exit Paragraph Mode": "Sair do modo de parágrafo",
|
||||
"Paragraph Mode": "Modo de parágrafo",
|
||||
"Embedding Model": "Modelo de incorporação",
|
||||
"{{count}} book(s) synced_one": "{{count}} livro sincronizado",
|
||||
"{{count}} book(s) synced_many": "{{count}} livros sincronizados",
|
||||
"{{count}} book(s) synced_other": "{{count}} livros sincronizados"
|
||||
}
|
||||
|
||||
@@ -986,5 +986,15 @@
|
||||
"Finished": "Прочитано",
|
||||
"Unread": "Не прочитано",
|
||||
"Clear Status": "Очистить статус",
|
||||
"Status": "Статус"
|
||||
"Status": "Статус",
|
||||
"Set status for {{count}} book(s)_few": "Установить статус для {{count}} книг",
|
||||
"Set status for {{count}} book(s)_many": "Установить статус для {{count}} книг",
|
||||
"Loading": "Загрузка...",
|
||||
"Exit Paragraph Mode": "Выйти из режима абзаца",
|
||||
"Paragraph Mode": "Режим абзаца",
|
||||
"Embedding Model": "Модель встраивания",
|
||||
"{{count}} book(s) synced_one": "{{count}} книга синхронизирована",
|
||||
"{{count}} book(s) synced_few": "{{count}} книги синхронизированы",
|
||||
"{{count}} book(s) synced_many": "{{count}} книг синхронизировано",
|
||||
"{{count}} book(s) synced_other": "{{count}} книг синхронизировано"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "අවසන්",
|
||||
"Unread": "නොකියවූ",
|
||||
"Clear Status": "තත්ත්වය හිස් කරන්න",
|
||||
"Status": "තත්ත්වය"
|
||||
"Status": "තත්ත්වය",
|
||||
"Loading": "පූරණය වෙමින් පවතී...",
|
||||
"Exit Paragraph Mode": "ඡේද ප්රකාරයෙන් ඉවත් වන්න",
|
||||
"Paragraph Mode": "ඡේද ප්රකාරය",
|
||||
"Embedding Model": "එබ්බවීමේ ආකෘතිය",
|
||||
"{{count}} book(s) synced_one": "{{count}} පොතක් සමමුහුර්ත කරන ලදී",
|
||||
"{{count}} book(s) synced_other": "{{count}} පොත් සමමුහුර්ත කරන ලදී"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "Läst",
|
||||
"Unread": "Oläst",
|
||||
"Clear Status": "Rensa status",
|
||||
"Status": "Status"
|
||||
"Status": "Status",
|
||||
"Loading": "Laddar...",
|
||||
"Exit Paragraph Mode": "Avsluta styckeläge",
|
||||
"Paragraph Mode": "Styckeläge",
|
||||
"Embedding Model": "Inbäddningsmodell",
|
||||
"{{count}} book(s) synced_one": "{{count}} bok synkroniserad",
|
||||
"{{count}} book(s) synced_other": "{{count}} böcker synkroniserade"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "முடிந்தது",
|
||||
"Unread": "படிக்கவில்லை",
|
||||
"Clear Status": "நிலையை அழி",
|
||||
"Status": "நிலை"
|
||||
"Status": "நிலை",
|
||||
"Loading": "ஏற்றுகிறது...",
|
||||
"Exit Paragraph Mode": "பத்தி பயன்முறையிலிருந்து வெளியேறு",
|
||||
"Paragraph Mode": "பத்தி பயன்முறை",
|
||||
"Embedding Model": "உட்பொதி மாதிரி",
|
||||
"{{count}} book(s) synced_one": "{{count}} புத்தகம் ஒத்திசைக்கப்பட்டது",
|
||||
"{{count}} book(s) synced_other": "{{count}} புத்தகங்கள் ஒத்திசைக்கப்பட்டன"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "โมเดล AI Gateway",
|
||||
"Actions": "การกระทำ",
|
||||
"Navigation": "การนำทาง",
|
||||
"Set status for {{count}} book(s)_one": "ตั้งสถานะสำหรับหนังสือ {{count}} เล่ม",
|
||||
"Set status for {{count}} book(s)_other": "ตั้งสถานะสำหรับหนังสือ {{count}} เล่ม",
|
||||
"Mark as Unread": "ทำเครื่องหมายว่ายังไม่อ่าน",
|
||||
"Mark as Finished": "ทำเครื่องหมายว่าอ่านจบแล้ว",
|
||||
"Finished": "อ่านจบแล้ว",
|
||||
"Unread": "ยังไม่ได้อ่าน",
|
||||
"Clear Status": "ล้างสถานะ",
|
||||
"Status": "สถานะ"
|
||||
"Status": "สถานะ",
|
||||
"Loading": "กำลังโหลด...",
|
||||
"Exit Paragraph Mode": "ออกจากโหมดพารากราฟ",
|
||||
"Paragraph Mode": "โหมดพารากราฟ",
|
||||
"Embedding Model": "โมเดลการฝังตัว",
|
||||
"{{count}} book(s) synced_other": "ซิงค์หนังสือ {{count}} เล่มแล้ว"
|
||||
}
|
||||
|
||||
@@ -966,5 +966,11 @@
|
||||
"Finished": "Bitirildi",
|
||||
"Unread": "Okunmadı",
|
||||
"Clear Status": "Durumu Temizle",
|
||||
"Status": "Durum"
|
||||
"Status": "Durum",
|
||||
"Loading": "Yükleniyor...",
|
||||
"Exit Paragraph Mode": "Paragraf modundan çık",
|
||||
"Paragraph Mode": "Paragraf modu",
|
||||
"Embedding Model": "Gömme Modeli",
|
||||
"{{count}} book(s) synced_one": "{{count}} kitap senkronize edildi",
|
||||
"{{count}} book(s) synced_other": "{{count}} kitap senkronize edildi"
|
||||
}
|
||||
|
||||
@@ -986,5 +986,15 @@
|
||||
"Finished": "Завершено",
|
||||
"Unread": "Непрочитане",
|
||||
"Clear Status": "Очистити статус",
|
||||
"Status": "Статус"
|
||||
"Status": "Статус",
|
||||
"Set status for {{count}} book(s)_few": "Встановити статус для {{count}} книг",
|
||||
"Set status for {{count}} book(s)_many": "Встановити статус для {{count}} книг",
|
||||
"Loading": "Завантаження...",
|
||||
"Exit Paragraph Mode": "Вийти з режиму абзацу",
|
||||
"Paragraph Mode": "Режим абзацу",
|
||||
"Embedding Model": "Модель вбудовування",
|
||||
"{{count}} book(s) synced_one": "{{count}} книга синхронізована",
|
||||
"{{count}} book(s) synced_few": "{{count}} книги синхронізовані",
|
||||
"{{count}} book(s) synced_many": "{{count}} книг синхронізовано",
|
||||
"{{count}} book(s) synced_other": "{{count}} книг синхронізовано"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "Mô hình AI Gateway",
|
||||
"Actions": "Hành động",
|
||||
"Navigation": "Điều hướng",
|
||||
"Set status for {{count}} book(s)_one": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Set status for {{count}} book(s)_other": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Mark as Unread": "Đánh dấu là chưa đọc",
|
||||
"Mark as Finished": "Đánh dấu là đã hoàn thành",
|
||||
"Finished": "Đã hoàn thành",
|
||||
"Unread": "Chưa đọc",
|
||||
"Clear Status": "Xóa trạng thái",
|
||||
"Status": "Trạng thái"
|
||||
"Status": "Trạng thái",
|
||||
"Loading": "Đang tải...",
|
||||
"Exit Paragraph Mode": "Thoát chế độ đoạn văn",
|
||||
"Paragraph Mode": "Chế độ đoạn văn",
|
||||
"Embedding Model": "Mô hình nhúng",
|
||||
"{{count}} book(s) synced_other": "Đã đồng bộ {{count}} cuốn sách"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "导航",
|
||||
"Set status for {{count}} book(s)_one": "设置 {{count}} 本书的状态",
|
||||
"Set status for {{count}} book(s)_other": "设置 {{count}} 本书的状态",
|
||||
"Mark as Unread": "标记为未读",
|
||||
"Mark as Finished": "标记为已读",
|
||||
"Finished": "已读",
|
||||
"Unread": "未读",
|
||||
"Clear Status": "清除状态",
|
||||
"Status": "状态"
|
||||
"Status": "状态",
|
||||
"Loading": "正在加载",
|
||||
"Exit Paragraph Mode": "退出段落模式",
|
||||
"Paragraph Mode": "段落模式",
|
||||
"Embedding Model": "嵌入模型",
|
||||
"{{count}} book(s) synced_other": "{{count}} 本书已同步"
|
||||
}
|
||||
|
||||
@@ -949,12 +949,16 @@
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "導覽",
|
||||
"Set status for {{count}} book(s)_one": "設定 {{count}} 本書的狀態",
|
||||
"Set status for {{count}} book(s)_other": "設定 {{count}} 本書的狀態",
|
||||
"Mark as Unread": "標示為未讀",
|
||||
"Mark as Finished": "標示為已讀",
|
||||
"Finished": "已讀",
|
||||
"Unread": "未讀",
|
||||
"Clear Status": "清除狀態",
|
||||
"Status": "狀態"
|
||||
"Status": "狀態",
|
||||
"Loading": "正在載入",
|
||||
"Exit Paragraph Mode": "退出段落模式",
|
||||
"Paragraph Mode": "段落模式",
|
||||
"Embedding Model": "嵌入模型",
|
||||
"{{count}} book(s) synced_other": "{{count}} 本書已同步"
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import { useSync } from '@/hooks/useSync';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
export const useBooksSync = () => {
|
||||
const _ = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { appService } = useEnv();
|
||||
const { library, isSyncing, libraryLoaded } = useLibraryStore();
|
||||
@@ -31,20 +34,33 @@ export const useBooksSync = () => {
|
||||
};
|
||||
}, [user, lastSyncedAtBooks]);
|
||||
|
||||
const pullLibrary = useCallback(async () => {
|
||||
if (!user) return;
|
||||
if (isPullingRef.current) {
|
||||
console.log('Pull already in progress, skipping...');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
isPullingRef.current = true;
|
||||
const library = useLibraryStore.getState().library;
|
||||
await syncBooks([], 'pull', libraryLoaded && library.length === 0 ? 0 : undefined);
|
||||
} finally {
|
||||
isPullingRef.current = false;
|
||||
}
|
||||
}, [user, libraryLoaded, syncBooks]);
|
||||
const pullLibrary = useCallback(
|
||||
async (fullRefresh = false, verbose = false) => {
|
||||
if (!user) return;
|
||||
if (isPullingRef.current) {
|
||||
console.log('Pull already in progress, skipping...');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
isPullingRef.current = true;
|
||||
const library = useLibraryStore.getState().library;
|
||||
const syncedBooksCount = await syncBooks(
|
||||
[],
|
||||
'pull',
|
||||
(libraryLoaded && library.length === 0) || fullRefresh ? 0 : undefined,
|
||||
);
|
||||
if (verbose) {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('{{count}} book(s) synced', { count: syncedBooksCount }),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
isPullingRef.current = false;
|
||||
}
|
||||
},
|
||||
[_, user, libraryLoaded, syncBooks],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const handleAutoSync = useCallback(
|
||||
|
||||
@@ -132,7 +132,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
const { pullLibrary, pushLibrary } = useBooksSync();
|
||||
const { isDragging } = useDragDropImport();
|
||||
|
||||
usePullToRefresh(containerRef, pullLibrary);
|
||||
usePullToRefresh(
|
||||
containerRef,
|
||||
pullLibrary.bind(null, false, true),
|
||||
pullLibrary.bind(null, true, true),
|
||||
);
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useShortcuts({
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
const TRIGGER_THRESHOLD = 120;
|
||||
const SHOW_INDICATOR_THRESHOLD = 60;
|
||||
const TRIGGER_THRESHOLD_STAGE1 = 120;
|
||||
const TRIGGER_THRESHOLD_STAGE2 = 400;
|
||||
const MAX_LOADING_POSITION = 80;
|
||||
const PARALLAX_EFFECT = 0.3;
|
||||
|
||||
const MAX = 128;
|
||||
const k = 0.4;
|
||||
function appr(x: number) {
|
||||
return MAX * (1 - Math.exp((-k * x) / MAX));
|
||||
// Platform-specific damping parameters for pull resistance
|
||||
const getPlatformDamping = () => {
|
||||
const platform = getOSPlatform();
|
||||
|
||||
if (platform === 'ios') {
|
||||
// iOS - tighter resistance (lower k = more damping)
|
||||
return { MAX: 120, k: 0.35 };
|
||||
} else if (platform === 'android') {
|
||||
// Android - looser resistance (higher k = less damping)
|
||||
return { MAX: 140, k: 0.5 };
|
||||
}
|
||||
return { MAX: 128, k: 0.4 };
|
||||
};
|
||||
|
||||
function createApprFunction(MAX: number, k: number) {
|
||||
return (x: number) => MAX * (1 - Math.exp((-k * x) / MAX));
|
||||
}
|
||||
|
||||
export const usePullToRefresh = (
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
onTrigger: () => void,
|
||||
onTriggerStage1: () => Promise<void> | void,
|
||||
onTriggerStage2?: () => Promise<void> | void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const damping = getPlatformDamping();
|
||||
const appr = createApprFunction(damping.MAX, damping.k);
|
||||
let isLoading = false;
|
||||
|
||||
el.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
|
||||
function handleTouchStart(startEvent: TouchEvent) {
|
||||
@@ -34,6 +54,7 @@ export const usePullToRefresh = (
|
||||
function handleTouchMove(moveEvent: TouchEvent) {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (isLoading) return;
|
||||
|
||||
const currentX = moveEvent.touches[0]!.clientX;
|
||||
const currentY = moveEvent.touches[0]!.clientY;
|
||||
@@ -41,79 +62,174 @@ export const usePullToRefresh = (
|
||||
const dy = currentY - initialY;
|
||||
if (dy < 0 || Math.abs(dx) * 2 > Math.abs(dy)) return;
|
||||
|
||||
const transformValue = appr(dy);
|
||||
|
||||
const parentEl = el.parentNode as HTMLDivElement;
|
||||
if (dy > TRIGGER_THRESHOLD) {
|
||||
flipArrow(parentEl);
|
||||
} else if (dy > SHOW_INDICATOR_THRESHOLD) {
|
||||
addPullIndicator(parentEl);
|
||||
if (dy > 10) {
|
||||
const opacity = Math.min(1, 0.3 + (dy / TRIGGER_THRESHOLD_STAGE2) * 0.7);
|
||||
showLoadingSpinner(parentEl, transformValue, opacity);
|
||||
} else {
|
||||
removePullIndicator(parentEl);
|
||||
hideLoadingSpinner(parentEl);
|
||||
}
|
||||
|
||||
// Update loading spinner position and opacity with parallax if it exists
|
||||
updateLoadingSpinnerPosition(parentEl, transformValue, dy);
|
||||
|
||||
const wrapper = el.querySelector('.transform-wrapper') as HTMLElement;
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = `translate3d(0, ${appr(dy)}px, 0)`;
|
||||
wrapper.style.transform = `translate3d(0, ${transformValue}px, 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
function addPullIndicator(el: HTMLDivElement) {
|
||||
const indicator = el.querySelector('.pull-indicator');
|
||||
if (indicator) {
|
||||
if (indicator.classList.contains('flip')) {
|
||||
indicator.classList.remove('flip');
|
||||
}
|
||||
return;
|
||||
}
|
||||
function showLoadingSpinner(el: HTMLDivElement, transform: number, opacity: number = 1) {
|
||||
const existing = el.querySelector('.pull-refresh-loading');
|
||||
if (existing) return;
|
||||
|
||||
const headerbar = document.querySelector('.titlebar');
|
||||
const pullIndicator = document.createElement('div');
|
||||
const loadingSpinner = document.createElement('div');
|
||||
const headerBottom = headerbar?.getBoundingClientRect().bottom || 0;
|
||||
pullIndicator.style.top = `${headerBottom + 30}px`;
|
||||
pullIndicator.className = 'pull-indicator text-gray-500';
|
||||
pullIndicator.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 19c-.3 0-.6-.1-.8-.3l-6-6c-.4-.4-.4-1 0-1.4s1-.4 1.4 0L11 16.2V5c0-.6.4-1 1-1s1 .4 1 1v11.2l4.4-4.4c.4-.4 1-.4 1.4 0s.4 1 0 1.4l-6 6c-.2.2-.5.3-.8.3z"/>
|
||||
</svg>
|
||||
`;
|
||||
el.appendChild(pullIndicator);
|
||||
const parallaxOffset = transform * PARALLAX_EFFECT;
|
||||
loadingSpinner.style.top = `${headerBottom + parallaxOffset}px`;
|
||||
loadingSpinner.style.transition = 'opacity 0.15s ease-out';
|
||||
loadingSpinner.style.opacity = opacity.toString();
|
||||
loadingSpinner.className = 'pull-refresh-loading';
|
||||
loadingSpinner.innerHTML = `<span class="loading loading-infinity loading-lg"></span>`;
|
||||
el.appendChild(loadingSpinner);
|
||||
}
|
||||
|
||||
function removePullIndicator(el: HTMLDivElement) {
|
||||
const pullIndicator = el.querySelector('.pull-indicator');
|
||||
if (pullIndicator) {
|
||||
pullIndicator.remove();
|
||||
function updateLoadingSpinnerPosition(el: HTMLDivElement, transform: number, dy: number) {
|
||||
const loadingSpinner = el.querySelector('.pull-refresh-loading') as HTMLElement;
|
||||
if (!loadingSpinner) return;
|
||||
|
||||
const headerbar = document.querySelector('.titlebar');
|
||||
const headerBottom = headerbar?.getBoundingClientRect().bottom || 0;
|
||||
const parallaxOffset = transform * PARALLAX_EFFECT;
|
||||
const opacity = Math.min(1, 0.2 + (dy / TRIGGER_THRESHOLD_STAGE2) * 0.8);
|
||||
loadingSpinner.style.top = `${headerBottom + parallaxOffset}px`;
|
||||
loadingSpinner.style.opacity = opacity.toString();
|
||||
}
|
||||
|
||||
function hideLoadingSpinner(el: HTMLDivElement) {
|
||||
const loadingSpinner = el.querySelector('.pull-refresh-loading');
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function flipArrow(el: HTMLDivElement) {
|
||||
const pullIndicator = el.querySelector('.pull-indicator');
|
||||
if (pullIndicator && !pullIndicator.classList.contains('flip')) {
|
||||
pullIndicator.classList.add('flip');
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(endEvent: TouchEvent) {
|
||||
async function handleTouchEnd(endEvent: TouchEvent) {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const wrapper = el.querySelector('.transform-wrapper') as HTMLElement;
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = 'translateY(0)';
|
||||
}
|
||||
removePullIndicator(el.parentNode as HTMLDivElement);
|
||||
|
||||
el.style.transition = 'transform 0.2s';
|
||||
const parentEl = el.parentNode as HTMLDivElement;
|
||||
|
||||
const y = endEvent.changedTouches[0]!.clientY;
|
||||
const dy = y - initialY;
|
||||
if (dy > TRIGGER_THRESHOLD) {
|
||||
onTrigger();
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
el.removeEventListener('touchmove', handleTouchMove);
|
||||
el.removeEventListener('touchend', handleTouchEnd);
|
||||
|
||||
const isStage2 = onTriggerStage2 && dy > TRIGGER_THRESHOLD_STAGE2;
|
||||
const isStage1 = dy > TRIGGER_THRESHOLD_STAGE1;
|
||||
|
||||
if (isStage2 || isStage1) {
|
||||
isLoading = true;
|
||||
|
||||
// Calculate current transform value with damping
|
||||
const transformValue = appr(dy);
|
||||
const targetPosition = Math.min(transformValue, MAX_LOADING_POSITION);
|
||||
|
||||
if (wrapper) {
|
||||
wrapper.style.transition = 'transform 0.2s ease-out';
|
||||
wrapper.style.transform = `translateY(${targetPosition}px)`;
|
||||
}
|
||||
|
||||
const loadingSpinner = parentEl.querySelector('.pull-refresh-loading') as HTMLElement;
|
||||
if (loadingSpinner) {
|
||||
const headerbar = document.querySelector('.titlebar');
|
||||
const headerBottom = headerbar?.getBoundingClientRect().bottom || 0;
|
||||
const parallaxOffset = targetPosition * PARALLAX_EFFECT;
|
||||
loadingSpinner.style.transition = 'top 0.2s ease-out';
|
||||
loadingSpinner.style.top = `${headerBottom + parallaxOffset}px`;
|
||||
loadingSpinner.style.opacity = '1';
|
||||
|
||||
// Remove transition after snap animation completes for smooth touch tracking
|
||||
setTimeout(() => {
|
||||
if (loadingSpinner) {
|
||||
loadingSpinner.style.transition = 'opacity 0.15s ease-out';
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Add touch listeners during loading to detect pull up and update parallax
|
||||
let loadingTouchStartY = 0;
|
||||
const handleLoadingTouchStart = (e: TouchEvent) => {
|
||||
loadingTouchStartY = e.touches[0]!.clientY;
|
||||
};
|
||||
const handleLoadingTouchMove = (e: TouchEvent) => {
|
||||
const currentY = e.touches[0]!.clientY;
|
||||
const pullDelta = currentY - loadingTouchStartY;
|
||||
|
||||
// Update both wrapper and spinner position to maintain parallax consistency
|
||||
const newTransform = targetPosition + pullDelta;
|
||||
if (newTransform > 0) {
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = `translateY(${newTransform}px)`;
|
||||
}
|
||||
|
||||
// Update spinner position with same parallax calculation
|
||||
const loadingSpinner = parentEl.querySelector('.pull-refresh-loading') as HTMLElement;
|
||||
if (loadingSpinner) {
|
||||
const headerbar = document.querySelector('.titlebar');
|
||||
const headerBottom = headerbar?.getBoundingClientRect().bottom || 0;
|
||||
const parallaxOffset = newTransform * PARALLAX_EFFECT;
|
||||
loadingSpinner.style.top = `${headerBottom + parallaxOffset}px`;
|
||||
loadingSpinner.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
// User pulled up significantly, reset
|
||||
if (pullDelta < -30) {
|
||||
if (wrapper) {
|
||||
wrapper.style.transition = 'transform 0.3s ease-out';
|
||||
wrapper.style.transform = 'translateY(0)';
|
||||
}
|
||||
hideLoadingSpinner(parentEl);
|
||||
el.removeEventListener('touchstart', handleLoadingTouchStart);
|
||||
el.removeEventListener('touchmove', handleLoadingTouchMove);
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('touchstart', handleLoadingTouchStart, { passive: true });
|
||||
el.addEventListener('touchmove', handleLoadingTouchMove, { passive: true });
|
||||
|
||||
try {
|
||||
const triggerFn = isStage2 ? onTriggerStage2 : onTriggerStage1;
|
||||
await Promise.resolve(triggerFn());
|
||||
} catch (error) {
|
||||
console.error('Pull to refresh error:', error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
hideLoadingSpinner(parentEl);
|
||||
if (wrapper) {
|
||||
wrapper.style.transition = 'transform 0.3s ease-out';
|
||||
wrapper.style.transform = 'translateY(0)';
|
||||
setTimeout(() => {
|
||||
if (wrapper) wrapper.style.transition = '';
|
||||
}, 300);
|
||||
}
|
||||
el.removeEventListener('touchstart', handleLoadingTouchStart);
|
||||
el.removeEventListener('touchmove', handleLoadingTouchMove);
|
||||
}
|
||||
} else {
|
||||
hideLoadingSpinner(parentEl);
|
||||
if (wrapper) {
|
||||
wrapper.style.transition = 'transform 0.2s';
|
||||
wrapper.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', onTransitionEnd);
|
||||
}
|
||||
}
|
||||
|
||||
function onTransitionEnd() {
|
||||
|
||||
@@ -110,7 +110,7 @@ export function useSync(bookKey?: string) {
|
||||
const result = await syncClient.pullChanges(since, type, bookId, metaHash);
|
||||
setSyncResult({ ...syncResult, [type]: result[type] });
|
||||
const records = result[type];
|
||||
if (since > 1000 && !records?.length) return;
|
||||
if (since > 1000 && !records?.length) return 0;
|
||||
// For since <= 1000, we set lastSyncedAt to now if no records returned
|
||||
const maxTime = records?.length ? computeMaxTimestamp(records) : Date.now();
|
||||
setLastSyncedAt(maxTime);
|
||||
@@ -140,6 +140,7 @@ export function useSync(bookKey?: string) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
return records?.length || 0;
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
if (err instanceof Error) {
|
||||
@@ -152,6 +153,7 @@ export function useSync(bookKey?: string) {
|
||||
} else {
|
||||
setSyncError(`Error pulling ${type}`);
|
||||
}
|
||||
return 0;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
saveSettings(envConfig, settings);
|
||||
@@ -184,13 +186,14 @@ export function useSync(bookKey?: string) {
|
||||
await pushChanges({ books });
|
||||
}
|
||||
if (op === 'pull' || op === 'both') {
|
||||
await pullChanges(
|
||||
return await pullChanges(
|
||||
'books',
|
||||
since ?? lastSyncedAtBooks + 1,
|
||||
setLastSyncedAtBooks,
|
||||
setSyncingBooks,
|
||||
);
|
||||
}
|
||||
return;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[lastSyncedAtInited, lastSyncedAtBooks],
|
||||
|
||||
@@ -296,7 +296,7 @@ foliate-fxl {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
.pull-refresh-loading {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
@@ -304,18 +304,9 @@ foliate-fxl {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-lighter);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.pull-indicator svg {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.pull-indicator.flip svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user