refactor(kosync): move kosync into reader sidebar menu and add manual pull/push trigger (#1912)
This commit is contained in:
@@ -459,7 +459,6 @@
|
||||
"Google Books": "كتب جوجل",
|
||||
"Open Library": "المكتبة المفتوحة",
|
||||
"Fiction, Science, History": "خيال، علوم، تاريخ",
|
||||
"Select Cover Image": "اختر صورة الغلاف",
|
||||
"Open Book in New Window": "فتح الكتاب في نافذة جديدة",
|
||||
"Voices for {{lang}}": "الأصوات لـ {{lang}}",
|
||||
"Yandex Translate": "ترجمة Yandex",
|
||||
@@ -500,7 +499,6 @@
|
||||
"File Content (recommended)": "محتوى الملف (موصى به)",
|
||||
"File Name": "اسم الملف",
|
||||
"Device Name": "اسم الجهاز",
|
||||
"Sync Tolerance": "تحمل المزامنة",
|
||||
"Connect to your KOReader Sync server.": "الاتصال بخادم مزامنة KOReader الخاص بك.",
|
||||
"Server URL": "عنوان URL للخادم",
|
||||
"Username": "اسم المستخدم",
|
||||
@@ -519,7 +517,6 @@
|
||||
"Approximately {{percentage}}%": "تقريبًا {{percentage}}%",
|
||||
"Failed to connect": "فشل في الاتصال",
|
||||
"Sync Server Connected": "تم الاتصال بخادم المزامنة",
|
||||
"Precision: {{precision}} digits after the decimal": "الدقة: {{precision}} أرقام بعد الفاصلة",
|
||||
"Sync as {{userDisplayName}}": "المزامنة كـ {{userDisplayName}}",
|
||||
"Custom Fonts": "الخطوط المخصصة",
|
||||
"Cancel Delete": "إلغاء الحذف",
|
||||
@@ -533,5 +530,7 @@
|
||||
"Select Video": "حدد فيديو",
|
||||
"Select Audio": "حدد صوت",
|
||||
"Select Fonts": "حدد الخطوط",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "تنسيقات الخط المدعومة: .ttf، .otf، .woff، .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "تنسيقات الخط المدعومة: .ttf، .otf، .woff، .woff2",
|
||||
"Push Progress": "تقدم الدفع",
|
||||
"Pull Progress": "تقدم السحب"
|
||||
}
|
||||
|
||||
@@ -438,7 +438,6 @@
|
||||
"Google Books": "Google དཔེ་མཛོད།",
|
||||
"Open Library": "སྒོ་འབྱེད་དཔེ་ཁང་།",
|
||||
"Fiction, Science, History": "སྒྲུང་གཏམ། ཚན་རིག ལོ་རྒྱུས།",
|
||||
"Select Cover Image": "སྟོང་ངོས་ཀྱི་པར་རིས་འདེམས་པ།",
|
||||
"Open Book in New Window": "སྒེའུ་ཁུང་གསར་པའི་ནང་དཔེ་དེབ་ཁ་ཕྱེ་བ།",
|
||||
"Voices for {{lang}}": "{{lang}} གྱི་སྒྲ་སྐད།",
|
||||
"Yandex Translate": "Yandex སྒྱུར་བཅོས།",
|
||||
@@ -482,8 +481,6 @@
|
||||
"File Content (recommended)": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"File Name": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"Device Name": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"Sync Tolerance": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"Precision: {{precision}} digits after the decimal": "ཡིག་སྣེ་འབྱུང་ཁུངས།",
|
||||
"Connect to your KOReader Sync server.": "ཁྱེད་ཀྱིས KOReader སྤྲི་དོན་སང་བཞིན་འདེམས་པ།",
|
||||
"Server URL": "སང་བཞིན་འདེམས་པའི URL",
|
||||
"Username": "ཁྱེད་ཀྱིས་འབྱུང་ཁུངས་འདེམས་པ།",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "བརྙན་ཟློས་འདེམས་པ།",
|
||||
"Select Audio": "སྒྲ་ཟློས་འདེམས་པ།",
|
||||
"Select Fonts": "ཡིག་གཟུགས་འདེམས་པ།",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "ཡིག་གཟུགས་འདེམས་པ་བྱས་ནས་བསྐྱར་བརྗེ་བ། ཡིག་གཟུགས་ཀྱི་སྒྲ་སྐད། .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "ཡིག་གཟུགས་འདེམས་པ་བྱས་ནས་བསྐྱར་བརྗེ་བ། ཡིག་གཟུགས་ཀྱི་སྒྲ་སྐད། .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "འདེགས་པའི་འཕེལ་རིམ།",
|
||||
"Pull Progress": "འདྲུད་པའི་འཕེལ་རིམ།"
|
||||
}
|
||||
|
||||
@@ -443,7 +443,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte",
|
||||
"Select Cover Image": "Coverbild auswählen",
|
||||
"Open Book in New Window": "Buch in neuem Fenster öffnen",
|
||||
"Voices for {{lang}}": "Stimmen für {{lang}}",
|
||||
"Yandex Translate": "Yandex Übersetzer",
|
||||
@@ -484,7 +483,6 @@
|
||||
"File Content (recommended)": "Dateiinhalte (empfohlen)",
|
||||
"File Name": "Dateiname",
|
||||
"Device Name": "Gerätename",
|
||||
"Sync Tolerance": "Sync-Toleranz",
|
||||
"Connect to your KOReader Sync server.": "Mit Ihrem KOReader Sync-Server verbinden.",
|
||||
"Server URL": "Server-URL",
|
||||
"Username": "Benutzername",
|
||||
@@ -503,7 +501,6 @@
|
||||
"Approximately {{percentage}}%": "Ungefähr {{percentage}}%",
|
||||
"Failed to connect": "Verbindung fehlgeschlagen",
|
||||
"Sync Server Connected": "Sync-Server verbunden",
|
||||
"Precision: {{precision}} digits after the decimal": "Präzision: {{precision}} Nachkommastellen",
|
||||
"Sync as {{userDisplayName}}": "Sync als {{userDisplayName}}",
|
||||
"Custom Fonts": "Benutzerdefinierte Schriftarten",
|
||||
"Cancel Delete": "Löschen abbrechen",
|
||||
@@ -517,5 +514,7 @@
|
||||
"Select Video": "Video auswählen",
|
||||
"Select Audio": "Audio auswählen",
|
||||
"Select Fonts": "Schriftarten auswählen",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Unterstützte Schriftartenformate: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Unterstützte Schriftartenformate: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Fortschritt senden",
|
||||
"Pull Progress": "Fortschritt empfangen"
|
||||
}
|
||||
|
||||
@@ -443,7 +443,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία",
|
||||
"Select Cover Image": "Επιλέξτε εικόνα εξωφύλλου",
|
||||
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο",
|
||||
"Voices for {{lang}}": "Φωνές για {{lang}}",
|
||||
"Yandex Translate": "Μετάφραση Yandex",
|
||||
@@ -484,7 +483,6 @@
|
||||
"File Content (recommended)": "Περιεχόμενο αρχείου (συνιστάται)",
|
||||
"File Name": "Όνομα αρχείου",
|
||||
"Device Name": "Όνομα συσκευής",
|
||||
"Sync Tolerance": "Ανοχή συγχρονισμού",
|
||||
"Connect to your KOReader Sync server.": "Συνδεθείτε στον διακομιστή συγχρονισμού KOReader.",
|
||||
"Server URL": "Διεύθυνση URL διακομιστή",
|
||||
"Username": "Όνομα χρήστη",
|
||||
@@ -503,7 +501,6 @@
|
||||
"Approximately {{percentage}}%": "Περίπου {{percentage}}%",
|
||||
"Failed to connect": "Αποτυχία σύνδεσης",
|
||||
"Sync Server Connected": "Ο διακομιστής συγχρονισμού είναι συνδεδεμένος",
|
||||
"Precision: {{precision}} digits after the decimal": "Ακρίβεια: {{precision}} ψηφία μετά την υποδιαστολή",
|
||||
"Sync as {{userDisplayName}}": "Συγχρονισμός ως {{userDisplayName}}",
|
||||
"Custom Fonts": "Προσαρμοσμένες Γραμματοσειρές",
|
||||
"Cancel Delete": "Ακύρωση Διαγραφής",
|
||||
@@ -517,5 +514,7 @@
|
||||
"Select Video": "Επιλογή Βίντεο",
|
||||
"Select Audio": "Επιλογή Ήχου",
|
||||
"Select Fonts": "Επιλογή Γραμματοσειρών",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Υποστηριζόμενες μορφές γραμματοσειρών: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Υποστηριζόμενες μορφές γραμματοσειρών: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Αποστολή προόδου",
|
||||
"Pull Progress": "Λήψη προόδου"
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
"File Content (recommended)": "Contenido del archivo (recomendado)",
|
||||
"File Name": "Nombre del archivo",
|
||||
"Device Name": "Nombre del dispositivo",
|
||||
"Sync Tolerance": "Tolerancia de sincronización",
|
||||
"Reading Progress Synced": "Progreso de lectura sincronizado",
|
||||
"Sync Conflict": "Conflicto de sincronización",
|
||||
"Sync reading progress from \"{{deviceName}}\"?": "¿Quieres sincronizar el progreso de lectura desde el dispositivo \"{{deviceName}}\"?",
|
||||
@@ -476,7 +475,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Ficción, Ciencia, Historia",
|
||||
"Select Cover Image": "Seleccionar imagen de portada",
|
||||
"Open Book in New Window": "Abre el libro en una nueva ventana",
|
||||
"Voices for {{lang}}": "Voces para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -507,7 +505,6 @@
|
||||
"Remove from Device Only": "Eliminar solo del dispositivo",
|
||||
"Failed to connect": "Error al conectar",
|
||||
"Sync Server Connected": "Servidor de sincronización conectado",
|
||||
"Precision: {{precision}} digits after the decimal": "Precisión: {{precision}} dígitos después del decimal",
|
||||
"Sync as {{userDisplayName}}": "Sincronizar como {{userDisplayName}}",
|
||||
"Custom Fonts": "Fuentes Personalizadas",
|
||||
"Cancel Delete": "Cancelar Eliminación",
|
||||
@@ -521,5 +518,7 @@
|
||||
"Select Video": "Seleccionar Video",
|
||||
"Select Audio": "Seleccionar Audio",
|
||||
"Select Fonts": "Seleccionar Fuentes",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fuente compatibles: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fuente compatibles: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Empujar progreso",
|
||||
"Pull Progress": "Tirar progreso"
|
||||
}
|
||||
|
||||
@@ -447,7 +447,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fiction, Science, Histoire",
|
||||
"Select Cover Image": "Sélectionner l'image de couverture",
|
||||
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre",
|
||||
"Voices for {{lang}}": "Voix pour {{lang}}",
|
||||
"Yandex Translate": "Yandex Traduction",
|
||||
@@ -488,7 +487,6 @@
|
||||
"File Content (recommended)": "Contenu du fichier (recommandé)",
|
||||
"File Name": "Nom du fichier",
|
||||
"Device Name": "Nom de l'appareil",
|
||||
"Sync Tolerance": "Tolérance de synchronisation",
|
||||
"Connect to your KOReader Sync server.": "Connectez-vous à votre serveur de synchronisation KOReader.",
|
||||
"Server URL": "URL du serveur",
|
||||
"Username": "Nom d'utilisateur",
|
||||
@@ -507,7 +505,6 @@
|
||||
"Approximately {{percentage}}%": "Environ {{percentage}}%",
|
||||
"Failed to connect": "Échec de la connexion",
|
||||
"Sync Server Connected": "Serveur de synchronisation connecté",
|
||||
"Precision: {{precision}} digits after the decimal": "Précision : {{precision}} chiffres après la virgule",
|
||||
"Sync as {{userDisplayName}}": "Synchroniser en tant que {{userDisplayName}}",
|
||||
"Custom Fonts": "Polices Personnalisées",
|
||||
"Cancel Delete": "Annuler la Suppression",
|
||||
@@ -521,5 +518,7 @@
|
||||
"Select Video": "Sélectionner une Vidéo",
|
||||
"Select Audio": "Sélectionner un Audio",
|
||||
"Select Fonts": "Sélectionner des Polices",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formats de police pris en charge : .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formats de police pris en charge : .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Pousser la Progression",
|
||||
"Pull Progress": "Tirer la Progression"
|
||||
}
|
||||
|
||||
@@ -443,7 +443,6 @@
|
||||
"Google Books": "गूगल बुक्स",
|
||||
"Open Library": "ओपन लाइब्रेरी",
|
||||
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास",
|
||||
"Select Cover Image": "कवर इमेज चुनें",
|
||||
"Open Book in New Window": "नई विंडो में पुस्तक खोलें",
|
||||
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें",
|
||||
"Yandex Translate": "यांडेक्स अनुवाद",
|
||||
@@ -484,7 +483,6 @@
|
||||
"File Content (recommended)": "फाइल सामग्री (अनुशंसित)",
|
||||
"File Name": "फाइल नाम",
|
||||
"Device Name": "डिवाइस नाम",
|
||||
"Sync Tolerance": "सिंक सहिष्णुता",
|
||||
"Connect to your KOReader Sync server.": "अपने KOReader सिंक सर्वर से कनेक्ट करें।",
|
||||
"Server URL": "सर्वर यूआरएल",
|
||||
"Username": "उपयोगकर्ता नाम",
|
||||
@@ -503,7 +501,6 @@
|
||||
"Approximately {{percentage}}%": "लगभग {{percentage}}%",
|
||||
"Failed to connect": "कनेक्ट करने में विफल",
|
||||
"Sync Server Connected": "सिंक सर्वर कनेक्टेड",
|
||||
"Precision: {{precision}} digits after the decimal": "सटीकता: {{precision}} दशमलव के बाद अंक",
|
||||
"Sync as {{userDisplayName}}": "सिंक के रूप में {{userDisplayName}}",
|
||||
"Custom Fonts": "कस्टम फ़ॉन्ट",
|
||||
"Cancel Delete": "डिलीट रद्द करें",
|
||||
@@ -517,5 +514,7 @@
|
||||
"Select Video": "वीडियो चुनें",
|
||||
"Select Audio": "ऑडियो चुनें",
|
||||
"Select Fonts": "फ़ॉन्ट चुनें",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "समर्थित फ़ॉन्ट प्रारूप: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "समर्थित फ़ॉन्ट प्रारूप: .ttf, .otf, .woff, .woff2",
|
||||
"Push progress": "पुश प्रगति",
|
||||
"Pull progress": "पुल प्रगति"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fiksi, Sains, Sejarah",
|
||||
"Select Cover Image": "Pilihan Gambar Sampul",
|
||||
"Open Book in New Window": "Buka Buku di Jendela Baru",
|
||||
"Voices for {{lang}}": "Suara untuk {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "Konten File (disarankan)",
|
||||
"File Name": "Nama File",
|
||||
"Device Name": "Nama Perangkat",
|
||||
"Sync Tolerance": "Toleransi Sinkronisasi",
|
||||
"Connect to your KOReader Sync server.": "Hubungkan ke server Sinkronisasi KOReader Anda.",
|
||||
"Server URL": "URL Server",
|
||||
"Username": "Nama Pengguna",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "Sekitar {{percentage}}%",
|
||||
"Failed to connect": "Gagal terhubung",
|
||||
"Sync Server Connected": "Server sinkronisasi terhubung",
|
||||
"Precision: {{precision}} digits after the decimal": "Presisi: {{precision}} digit setelah desimal",
|
||||
"Sync as {{userDisplayName}}": "Sinkronkan sebagai {{userDisplayName}}",
|
||||
"Custom Fonts": "Font Kustom",
|
||||
"Cancel Delete": "Batal Hapus",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "Pilih Video",
|
||||
"Select Audio": "Pilih Audio",
|
||||
"Select Fonts": "Pilih Font",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format font yang didukung: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format font yang didukung: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Dorong Progres",
|
||||
"Pull Progress": "Tarik Progres"
|
||||
}
|
||||
|
||||
@@ -447,7 +447,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Narrativa, Scienza, Storia",
|
||||
"Select Cover Image": "Seleziona immagine di copertina",
|
||||
"Open Book in New Window": "Apri libro in una nuova finestra",
|
||||
"Voices for {{lang}}": "Voci per {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -488,7 +487,6 @@
|
||||
"File Content (recommended)": "Contenuto del File (consigliato)",
|
||||
"File Name": "Nome del File",
|
||||
"Device Name": "Nome del Dispositivo",
|
||||
"Sync Tolerance": "Tolleranza di Sincronizzazione",
|
||||
"Connect to your KOReader Sync server.": "Connettiti al tuo server KOReader Sync.",
|
||||
"Server URL": "URL del Server",
|
||||
"Username": "Nome Utente",
|
||||
@@ -507,7 +505,6 @@
|
||||
"Approximately {{percentage}}%": "Circa {{percentage}}%",
|
||||
"Failed to connect": "Impossibile connettersi",
|
||||
"Sync Server Connected": "Server di sincronizzazione connesso",
|
||||
"Precision: {{precision}} digits after the decimal": "Precisione: {{precision}} cifre dopo la virgola",
|
||||
"Sync as {{userDisplayName}}": "Sincronizza come {{userDisplayName}}",
|
||||
"Custom Fonts": "Font Personalizzati",
|
||||
"Cancel Delete": "Annulla Eliminazione",
|
||||
@@ -521,5 +518,7 @@
|
||||
"Select Video": "Seleziona Video",
|
||||
"Select Audio": "Seleziona Audio",
|
||||
"Select Fonts": "Seleziona Font",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formati di font supportati: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formati di font supportati: .ttf, .otf, .woff, .woff2",
|
||||
"Push progress": "Progresso push",
|
||||
"Pull progress": "Progresso pull"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "小説、科学、歴史",
|
||||
"Select Cover Image": "表紙画像を選択",
|
||||
"Open Book in New Window": "新しいウィンドウで本を開く",
|
||||
"Voices for {{lang}}": "{{lang}}の音声",
|
||||
"Yandex Translate": "Yandex翻訳",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "ファイル内容(推奨)",
|
||||
"File Name": "ファイル名",
|
||||
"Device Name": "デバイス名",
|
||||
"Sync Tolerance": "同期許容範囲",
|
||||
"Connect to your KOReader Sync server.": "KOReader Syncサーバーに接続します。",
|
||||
"Server URL": "サーバーURL",
|
||||
"Username": "ユーザー名",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "おおよそ {{percentage}}%",
|
||||
"Failed to connect": "接続に失敗しました",
|
||||
"Sync Server Connected": "同期サーバーに接続されました",
|
||||
"Precision: {{precision}} digits after the decimal": "精度: {{precision}} 桁の小数点以下",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}} として同期",
|
||||
"Custom Fonts": "カスタムフォント",
|
||||
"Cancel Delete": "削除をキャンセル",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "動画を選択",
|
||||
"Select Audio": "音声を選択",
|
||||
"Select Fonts": "フォントを選択",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "サポートされているフォント形式:.ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "サポートされているフォント形式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "進捗をプッシュ",
|
||||
"Pull Progress": "進捗をプル"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "구글 북스",
|
||||
"Open Library": "오픈 라이브러리",
|
||||
"Fiction, Science, History": "소설, 과학, 역사",
|
||||
"Select Cover Image": "표지 이미지 선택",
|
||||
"Open Book in New Window": "새 창에서 책 열기",
|
||||
"Voices for {{lang}}": "{{lang}}의 음성",
|
||||
"Yandex Translate": "Yandex 번역",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "파일 내용 (권장)",
|
||||
"File Name": "파일 이름",
|
||||
"Device Name": "장치 이름",
|
||||
"Sync Tolerance": "동기화 허용 오차",
|
||||
"Connect to your KOReader Sync server.": "KOReader Sync 서버에 연결합니다.",
|
||||
"Server URL": "서버 URL",
|
||||
"Username": "사용자 이름",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "대략 {{percentage}}%",
|
||||
"Failed to connect": "연결 실패",
|
||||
"Sync Server Connected": "동기화 서버에 연결됨",
|
||||
"Precision: {{precision}} digits after the decimal": "정밀도: {{precision}} 자리의 소수점 이하",
|
||||
"Sync as {{userDisplayName}}": "{{userDisplayName}}로 동기화",
|
||||
"Custom Fonts": "사용자 정의 글꼴",
|
||||
"Cancel Delete": "삭제 취소",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "동영상 선택",
|
||||
"Select Audio": "오디오 선택",
|
||||
"Select Fonts": "글꼴 선택",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "지원되는 글꼴 형식: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "지원되는 글꼴 형식: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "진행 상황 푸시",
|
||||
"Pull Progress": "진행 상황 끌어오기"
|
||||
}
|
||||
|
||||
@@ -443,7 +443,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis",
|
||||
"Select Cover Image": "Selecteer omslagafbeelding",
|
||||
"Open Book in New Window": "Open boek in nieuw venster",
|
||||
"Voices for {{lang}}": "Stemmen voor {{lang}}",
|
||||
"Yandex Translate": "Yandex Vertalen",
|
||||
@@ -484,7 +483,6 @@
|
||||
"File Content (recommended)": "Bestandsinhoud (aanbevolen)",
|
||||
"File Name": "Bestandsnaam",
|
||||
"Device Name": "Apparaatnaam",
|
||||
"Sync Tolerance": "Synchronisatietolerantie",
|
||||
"Connect to your KOReader Sync server.": "Verbind met je KOReader Sync-server.",
|
||||
"Server URL": "Server-URL",
|
||||
"Username": "Gebruikersnaam",
|
||||
@@ -503,7 +501,6 @@
|
||||
"Approximately {{percentage}}%": "Ongeveer {{percentage}}%",
|
||||
"Failed to connect": "Verbinding mislukt",
|
||||
"Sync Server Connected": "Synchronisatie server verbonden",
|
||||
"Precision: {{precision}} digits after the decimal": "Precisie: {{precision}} cijfers na de komma",
|
||||
"Sync as {{userDisplayName}}": "Synchroniseren als {{userDisplayName}}",
|
||||
"Custom Fonts": "Aangepaste Lettertypen",
|
||||
"Cancel Delete": "Verwijderen Annuleren",
|
||||
@@ -517,5 +514,7 @@
|
||||
"Select Video": "Video Selecteren",
|
||||
"Select Audio": "Audio Selecteren",
|
||||
"Select Fonts": "Lettertypen Selecteren",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Ondersteunde lettertypeformaten: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Ondersteunde lettertypeformaten: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Voortgang pushen",
|
||||
"Pull Progress": "Voortgang pullen"
|
||||
}
|
||||
|
||||
@@ -451,7 +451,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Beletrystyka, Nauka, Historia",
|
||||
"Select Cover Image": "Zaznacz obraz okładki",
|
||||
"Open Book in New Window": "Otwórz książkę w nowym oknie",
|
||||
"Voices for {{lang}}": "{{lang}} Głosy",
|
||||
"Yandex Translate": "Yandex Tłumacz",
|
||||
@@ -492,7 +491,6 @@
|
||||
"File Content (recommended)": "Zawartość pliku (zalecane)",
|
||||
"File Name": "Nazwa pliku",
|
||||
"Device Name": "Nazwa urządzenia",
|
||||
"Sync Tolerance": "Tolerancja synchronizacji",
|
||||
"Connect to your KOReader Sync server.": "Połącz z serwerem synchronizacji KOReader.",
|
||||
"Server URL": "Adres URL serwera",
|
||||
"Username": "Nazwa użytkownika",
|
||||
@@ -511,7 +509,6 @@
|
||||
"Approximately {{percentage}}%": "Około {{percentage}}%",
|
||||
"Failed to connect": "Nie udało się połączyć",
|
||||
"Sync Server Connected": "Serwer synchronizacji połączony",
|
||||
"Precision: {{precision}} digits after the decimal": "Precyzja: {{precision}} cyfr po przecinku",
|
||||
"Sync as {{userDisplayName}}": "Synchronizuj jako {{userDisplayName}}",
|
||||
"Custom Fonts": "Niestandardowe Czcionki",
|
||||
"Cancel Delete": "Anuluj Usuwanie",
|
||||
@@ -525,5 +522,7 @@
|
||||
"Select Video": "Wybierz Wideo",
|
||||
"Select Audio": "Wybierz Audio",
|
||||
"Select Fonts": "Wybierz Czcionki",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Obsługiwane formaty czcionek: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Obsługiwane formaty czcionek: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Wyślij postęp",
|
||||
"Pull Progress": "Pobierz postęp"
|
||||
}
|
||||
|
||||
@@ -447,7 +447,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Ficção, Ciência, História",
|
||||
"Select Cover Image": "Selecionar imagem da capa",
|
||||
"Open Book in New Window": "Abrir livro em nova janela",
|
||||
"Voices for {{lang}}": "Vozes para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -488,7 +487,6 @@
|
||||
"File Content (recommended)": "Conteúdo do Arquivo (recomendado)",
|
||||
"File Name": "Nome do Arquivo",
|
||||
"Device Name": "Nome do Dispositivo",
|
||||
"Sync Tolerance": "Tolerância de Sincronização",
|
||||
"Connect to your KOReader Sync server.": "Conectar ao seu servidor KOReader Sync.",
|
||||
"Server URL": "URL do Servidor",
|
||||
"Username": "Nome de Usuário",
|
||||
@@ -507,7 +505,6 @@
|
||||
"Approximately {{percentage}}%": "Aproximadamente {{percentage}}%",
|
||||
"Failed to connect": "Falha ao conectar",
|
||||
"Sync Server Connected": "Servidor de Sincronização Conectado",
|
||||
"Precision: {{precision}} digits after the decimal": "Precisão: {{precision}} dígitos após a vírgula",
|
||||
"Sync as {{userDisplayName}}": "Sincronizar como {{userDisplayName}}",
|
||||
"Custom Fonts": "Fontes Personalizadas",
|
||||
"Cancel Delete": "Cancelar Exclusão",
|
||||
@@ -521,5 +518,7 @@
|
||||
"Select Video": "Selecionar Vídeo",
|
||||
"Select Audio": "Selecionar Áudio",
|
||||
"Select Fonts": "Selecionar Fontes",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fonte suportados: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fonte suportados: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Enviar Progresso",
|
||||
"Pull Progress": "Obter Progresso"
|
||||
}
|
||||
|
||||
@@ -451,7 +451,6 @@
|
||||
"Google Books": "Google Книги",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Художественная литература, Наука, История",
|
||||
"Select Cover Image": "Выбрать изображение обложки",
|
||||
"Open Book in New Window": "Открыть книгу в новом окне",
|
||||
"Voices for {{lang}}": "Голоса для {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -492,7 +491,6 @@
|
||||
"File Content (recommended)": "Содержимое файла (рекомендуется)",
|
||||
"File Name": "Имя файла",
|
||||
"Device Name": "Имя устройства",
|
||||
"Sync Tolerance": "Допуск синхронизации",
|
||||
"Connect to your KOReader Sync server.": "Подключитесь к вашему серверу синхронизации KOReader.",
|
||||
"Server URL": "URL сервера",
|
||||
"Username": "Имя пользователя",
|
||||
@@ -511,7 +509,6 @@
|
||||
"Approximately {{percentage}}%": "Приблизительно {{percentage}}%",
|
||||
"Failed to connect": "Не удалось подключиться",
|
||||
"Sync Server Connected": "Сервер синхронизации подключен",
|
||||
"Precision: {{precision}} digits after the decimal": "Точность: {{precision}} знаков после запятой",
|
||||
"Sync as {{userDisplayName}}": "Синхронизировать как {{userDisplayName}}",
|
||||
"Custom Fonts": "Пользовательские Шрифты",
|
||||
"Cancel Delete": "Отменить Удаление",
|
||||
@@ -525,5 +522,7 @@
|
||||
"Select Video": "Выбрать Видео",
|
||||
"Select Audio": "Выбрать Аудио",
|
||||
"Select Fonts": "Выбрать Шрифты",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Поддерживаемые форматы шрифтов: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Поддерживаемые форматы шрифтов: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Отправить прогресс",
|
||||
"Pull Progress": "Получить прогресс"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์",
|
||||
"Select Cover Image": "เลือกภาพปก",
|
||||
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่",
|
||||
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "เนื้อหาไฟล์ (แนะนำ)",
|
||||
"File Name": "ชื่อไฟล์",
|
||||
"Device Name": "ชื่ออุปกรณ์",
|
||||
"Sync Tolerance": "ความทนทานต่อการซิงค์",
|
||||
"Connect to your KOReader Sync server.": "เชื่อมต่อกับเซิร์ฟเวอร์ KOReader Sync ของคุณ",
|
||||
"Server URL": "URL ของเซิร์ฟเวอร์",
|
||||
"Username": "ชื่อผู้ใช้",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "ประมาณ {{percentage}}%",
|
||||
"Failed to connect": "ไม่สามารถเชื่อมต่อได้",
|
||||
"Sync Server Connected": "เซิร์ฟเวอร์ซิงค์เชื่อมต่อแล้ว",
|
||||
"Precision: {{precision}} digits after the decimal": "ความแม่นยำ: {{precision}} หลักหลังจุดทศนิยม",
|
||||
"Sync as {{userDisplayName}}": "ซิงค์เป็น {{userDisplayName}}",
|
||||
"Custom Fonts": "ฟอนต์กำหนดเอง",
|
||||
"Cancel Delete": "ยกเลิกการลบ",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "เลือกวิดีโอ",
|
||||
"Select Audio": "เลือกเสียง",
|
||||
"Select Fonts": "เลือกฟอนต์",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "รูปแบบฟอนต์ที่รองรับ: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "รูปแบบฟอนต์ที่รองรับ: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "ส่งความคืบหน้า",
|
||||
"Pull Progress": "ดึงความคืบหน้า"
|
||||
}
|
||||
|
||||
@@ -443,7 +443,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Kurgu, Bilim, Tarih",
|
||||
"Select Cover Image": "Kapak Resmi Seç",
|
||||
"Open Book in New Window": "Kitabı Yeni Pencerede Aç",
|
||||
"Voices for {{lang}}": "{{lang}} için Sesler",
|
||||
"Yandex Translate": "Yandex Çeviri",
|
||||
@@ -484,7 +483,6 @@
|
||||
"File Content (recommended)": "Dosya İçeriği (önerilen)",
|
||||
"File Name": "Dosya Adı",
|
||||
"Device Name": "Cihaz Adı",
|
||||
"Sync Tolerance": "Senkronizasyon Toleransı",
|
||||
"Connect to your KOReader Sync server.": "KOReader Senkronizasyon sunucunuza bağlanın.",
|
||||
"Server URL": "Sunucu URL'si",
|
||||
"Username": "Kullanıcı Adı",
|
||||
@@ -503,7 +501,6 @@
|
||||
"Approximately {{percentage}}%": "Yaklaşık {{percentage}}%",
|
||||
"Failed to connect": "Bağlanma başarısız oldu",
|
||||
"Sync Server Connected": "Senkronizasyon Sunucusu Bağlandı",
|
||||
"Precision: {{precision}} digits after the decimal": "Hassasiyet: {{precision}} ondalık basamaktan sonra",
|
||||
"Sync as {{userDisplayName}}": "Senkronize et {{userDisplayName}} olarak",
|
||||
"Custom Fonts": "Özel Yazı Tipleri",
|
||||
"Cancel Delete": "Silmeyi İptal Et",
|
||||
@@ -517,5 +514,7 @@
|
||||
"Select Video": "Video Seç",
|
||||
"Select Audio": "Ses Seç",
|
||||
"Select Fonts": "Yazı Tiplerini Seç",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Desteklenen yazı tipi formatları: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Desteklenen yazı tipi formatları: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "İlerlemeyi Gönder",
|
||||
"Pull Progress": "İlerlemeyi Al"
|
||||
}
|
||||
|
||||
@@ -451,7 +451,6 @@
|
||||
"Google Books": "Google Книги",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Художня література, Наукова фантастика, Історія",
|
||||
"Select Cover Image": "Оберіть зображення обкладинки",
|
||||
"Open Book in New Window": "Відкрити книгу в новому вікні",
|
||||
"Voices for {{lang}}": "Голоси для {{lang}}",
|
||||
"Yandex Translate": "Yandex Перекладач (росія)",
|
||||
@@ -492,7 +491,6 @@
|
||||
"File Content (recommended)": "Вміст файлу (рекомендується)",
|
||||
"File Name": "Ім'я файлу",
|
||||
"Device Name": "Ім'я пристрою",
|
||||
"Sync Tolerance": "Допустима похибка синхронізації",
|
||||
"Connect to your KOReader Sync server.": "Підключіться до свого сервера синхронізації KOReader.",
|
||||
"Server URL": "URL сервера",
|
||||
"Username": "Ім'я користувача",
|
||||
@@ -511,7 +509,6 @@
|
||||
"Approximately {{percentage}}%": "Приблизно {{percentage}}%",
|
||||
"Failed to connect": "Не вдалося під'єднатися",
|
||||
"Sync Server Connected": "Сервер синхронізації під'єднано",
|
||||
"Precision: {{precision}} digits after the decimal": "Точність: {{precision}} знаків після коми",
|
||||
"Sync as {{userDisplayName}}": "Синхронізувати як {{userDisplayName}}",
|
||||
"Custom Fonts": "Користувацькі Шрифти",
|
||||
"Cancel Delete": "Скасувати Видалення",
|
||||
@@ -525,5 +522,7 @@
|
||||
"Select Video": "Вибрати Відео",
|
||||
"Select Audio": "Вибрати Аудіо",
|
||||
"Select Fonts": "Вибрати Шрифти",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Підтримувані формати шрифтів: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Підтримувані формати шрифтів: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Надіслати прогрес",
|
||||
"Pull Progress": "Отримати прогрес"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "Google Books",
|
||||
"Open Library": "Open Library",
|
||||
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử",
|
||||
"Select Cover Image": "Chọn ảnh bìa",
|
||||
"Open Book in New Window": "Mở Sách trong Cửa sổ Mới",
|
||||
"Voices for {{lang}}": "Giọng nói cho {{lang}}",
|
||||
"Yandex Translate": "Yandex Dịch",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "Nội dung tệp (được khuyến nghị)",
|
||||
"File Name": "Tên tệp",
|
||||
"Device Name": "Tên thiết bị",
|
||||
"Sync Tolerance": "Độ dung sai đồng bộ",
|
||||
"Connect to your KOReader Sync server.": "Kết nối với máy chủ đồng bộ KOReader của bạn.",
|
||||
"Server URL": "URL máy chủ",
|
||||
"Username": "Tên người dùng",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "Khoảng {{percentage}}%",
|
||||
"Failed to connect": "Không thể kết nối",
|
||||
"Sync Server Connected": "Máy chủ đồng bộ đã kết nối",
|
||||
"Precision: {{precision}} digits after the decimal": "Độ chính xác: {{precision}} chữ số sau dấu thập phân",
|
||||
"Sync as {{userDisplayName}}": "Đồng bộ dưới tên {{userDisplayName}}",
|
||||
"Custom Fonts": "Phông Chữ Tùy Chỉnh",
|
||||
"Cancel Delete": "Hủy Xóa",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "Chọn Video",
|
||||
"Select Audio": "Chọn Âm Thanh",
|
||||
"Select Fonts": "Chọn Phông Chữ",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Định dạng phông chữ được hỗ trợ: .ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "Định dạng phông chữ được hỗ trợ: .ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "Đẩy tiến độ",
|
||||
"Pull Progress": "Kéo tiến độ"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "谷歌图书",
|
||||
"Open Library": "开放图书馆",
|
||||
"Fiction, Science, History": "小说,科学,历史",
|
||||
"Select Cover Image": "选择封面图片",
|
||||
"Open Book in New Window": "在新窗口中打开书籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的语音",
|
||||
"Yandex Translate": "Yandex 翻译",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "文件内容(推荐)",
|
||||
"File Name": "文件名",
|
||||
"Device Name": "设备名称",
|
||||
"Sync Tolerance": "同步容忍度",
|
||||
"Connect to your KOReader Sync server.": "连接到您的 KOReader 同步服务器。",
|
||||
"Server URL": "服务器 URL",
|
||||
"Username": "用户名",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "大约 {{percentage}}%",
|
||||
"Failed to connect": "连接失败",
|
||||
"Sync Server Connected": "同步服务器已连接",
|
||||
"Precision: {{precision}} digits after the decimal": "精度:{{precision}} 位小数",
|
||||
"Sync as {{userDisplayName}}": "以 {{userDisplayName}} 身份同步",
|
||||
"Custom Fonts": "自定义字体",
|
||||
"Cancel Delete": "取消删除",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "选择视频",
|
||||
"Select Audio": "选择音频",
|
||||
"Select Fonts": "选择字体",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支持的字体格式:.ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支持的字体格式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "推送阅读进度",
|
||||
"Pull Progress": "拉取阅读进度"
|
||||
}
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"Google Books": "Google 圖書",
|
||||
"Open Library": "開放圖書館",
|
||||
"Fiction, Science, History": "小說,科學,歷史",
|
||||
"Select Cover Image": "選擇封面圖片",
|
||||
"Open Book in New Window": "在新視窗中打開書籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的語音",
|
||||
"Yandex Translate": "Yandex 翻譯",
|
||||
@@ -480,7 +479,6 @@
|
||||
"File Content (recommended)": "文件內容(推薦)",
|
||||
"File Name": "文件名",
|
||||
"Device Name": "設備名稱",
|
||||
"Sync Tolerance": "同步容忍度",
|
||||
"Connect to your KOReader Sync server.": "連接到您的 KOReader 同步伺服器。",
|
||||
"Server URL": "伺服器 URL",
|
||||
"Username": "用戶名",
|
||||
@@ -499,7 +497,6 @@
|
||||
"Approximately {{percentage}}%": "大約 {{percentage}}%",
|
||||
"Failed to connect": "連接失敗",
|
||||
"Sync Server Connected": "同步伺服器已連接",
|
||||
"Precision: {{precision}} digits after the decimal": "精度: {{precision}} 位小數",
|
||||
"Sync as {{userDisplayName}}": "以 {{userDisplayName}} 身份同步",
|
||||
"Custom Fonts": "自訂字型",
|
||||
"Cancel Delete": "取消刪除",
|
||||
@@ -513,5 +510,7 @@
|
||||
"Select Video": "選擇影片",
|
||||
"Select Audio": "選擇音訊",
|
||||
"Select Fonts": "選擇字型",
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支援的字型格式:.ttf, .otf, .woff, .woff2"
|
||||
"Supported font formats: .ttf, .otf, .woff, .woff2": "支援的字型格式:.ttf, .otf, .woff, .woff2",
|
||||
"Push Progress": "推送閱讀進度",
|
||||
"Pull Progress": "拉取閱讀進度"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { TbSunMoon } from 'react-icons/tb';
|
||||
import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { setKOSyncSettingsWindowVisible } from './KOSyncSettings';
|
||||
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
@@ -159,11 +158,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsTelemetryEnabled(settings.telemetryEnabled);
|
||||
};
|
||||
|
||||
const showKoSyncSettingsWindow = () => {
|
||||
setKOSyncSettingsWindowVisible(true);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const handleUpgrade = () => {
|
||||
navigateToProfile(router);
|
||||
setIsDropdownOpen?.(false);
|
||||
@@ -274,8 +268,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem label={_('KOReader Sync')} onClick={showKoSyncSettingsWindow} />
|
||||
<hr className='border-base-200 my-1' />
|
||||
{user && userPlan === 'free' && !appService?.isIOSApp && (
|
||||
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
|
||||
)}
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
} from '@/utils/window';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { KOSyncSettingsWindow } from './components/KOSyncSettings';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { BookDetailModal } from '@/components/metadata';
|
||||
@@ -730,7 +729,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
/>
|
||||
)}
|
||||
<AboutWindow />
|
||||
<KOSyncSettingsWindow />
|
||||
<UpdaterWindow />
|
||||
<Toast />
|
||||
</div>
|
||||
|
||||
+41
-95
@@ -1,17 +1,16 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { md5 } from 'js-md5';
|
||||
import clsx from 'clsx';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { KOSyncClient } from '@/services/sync/KOSyncClient';
|
||||
import { KoreaderSyncChecksumMethod, KoreaderSyncStrategy } from '@/types/settings';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { KOSyncChecksumMethod, KOSyncStrategy } from '@/types/settings';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import Dialog from '@/components/Dialog';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
@@ -69,19 +68,14 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [url, setUrl] = useState(settings.koreaderSyncServerUrl || '');
|
||||
const [username, setUsername] = useState(settings.koreaderSyncUsername || '');
|
||||
const [url, setUrl] = useState(settings.kosync.serverUrl || '');
|
||||
const [username, setUsername] = useState(settings.kosync.username || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionStatus, setConnectionStatus] = useState('');
|
||||
const [deviceName, setDeviceName] = useState('');
|
||||
const [osName, setOsName] = useState('');
|
||||
|
||||
const [toleranceSliderValue, setToleranceSliderValue] = useState(() => {
|
||||
const tolerance = settings.koreaderSyncPercentageTolerance;
|
||||
return tolerance && tolerance > 0 ? Math.round(-Math.log10(tolerance)) : 4;
|
||||
});
|
||||
|
||||
// Get the OS name once
|
||||
useEffect(() => {
|
||||
const formatOsName = (name: string): string => {
|
||||
@@ -108,13 +102,10 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const defaultName = osName ? `Readest (${osName})` : 'Readest';
|
||||
setDeviceName(settings.koreaderSyncDeviceName || defaultName);
|
||||
}, [settings.koreaderSyncDeviceName, osName]);
|
||||
setDeviceName(settings.kosync.deviceName || defaultName);
|
||||
}, [settings.kosync.deviceName, osName]);
|
||||
|
||||
const isConfigured = useMemo(
|
||||
() => !!settings.koreaderSyncUserkey,
|
||||
[settings.koreaderSyncUserkey],
|
||||
);
|
||||
const isConfigured = useMemo(() => !!settings.kosync.userkey, [settings.kosync.userkey]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedSaveDeviceName = useCallback(
|
||||
@@ -136,15 +127,10 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
setIsOpen(event.detail.visible);
|
||||
if (event.detail.visible) {
|
||||
setUrl(settings.koreaderSyncServerUrl || '');
|
||||
setUsername(settings.koreaderSyncUsername || '');
|
||||
setUrl(settings.kosync.serverUrl || '');
|
||||
setUsername(settings.kosync.username || '');
|
||||
setPassword('');
|
||||
setConnectionStatus('');
|
||||
// Sync the slider with the current settings when opening
|
||||
const tolerance = settings.koreaderSyncPercentageTolerance;
|
||||
setToleranceSliderValue(
|
||||
tolerance && tolerance > 0 ? Math.round(-Math.log10(tolerance)) : 4,
|
||||
);
|
||||
}
|
||||
};
|
||||
const el = document.getElementById('kosync_settings_window');
|
||||
@@ -152,40 +138,26 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
return () => {
|
||||
el?.removeEventListener('setKOSyncSettingsVisibility', handleCustomEvent as EventListener);
|
||||
};
|
||||
}, [
|
||||
settings.koreaderSyncServerUrl,
|
||||
settings.koreaderSyncUsername,
|
||||
settings.koreaderSyncPercentageTolerance,
|
||||
]);
|
||||
}, [settings.kosync.serverUrl, settings.kosync.username]);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setIsConnecting(true);
|
||||
|
||||
let deviceId = settings.koreaderSyncDeviceId;
|
||||
if (!deviceId) {
|
||||
deviceId = uuidv4().replace(/-/g, '').toUpperCase();
|
||||
}
|
||||
|
||||
const client = new KOSyncClient(
|
||||
url,
|
||||
const config = {
|
||||
...settings.kosync,
|
||||
serverUrl: url,
|
||||
username,
|
||||
md5(password),
|
||||
settings.koreaderSyncChecksumMethod,
|
||||
deviceId,
|
||||
userkey: md5(password),
|
||||
deviceName,
|
||||
);
|
||||
enabled: true,
|
||||
};
|
||||
const client = new KOSyncClient(config);
|
||||
const result = await client.connect(username, password);
|
||||
|
||||
if (result.success) {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
koreaderSyncServerUrl: url,
|
||||
koreaderSyncUsername: username,
|
||||
koreaderSyncUserkey: md5(password),
|
||||
koreaderSyncDeviceId: deviceId,
|
||||
koreaderSyncDeviceName: deviceName,
|
||||
koreaderSyncStrategy:
|
||||
settings.koreaderSyncStrategy === 'disabled' ? 'prompt' : settings.koreaderSyncStrategy,
|
||||
kosync: config,
|
||||
};
|
||||
setSettings(newSettings);
|
||||
await saveSettings(envConfig, newSettings);
|
||||
@@ -201,11 +173,12 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
koreaderSyncStrategy: 'disabled' as KoreaderSyncStrategy,
|
||||
koreaderSyncUserkey: '',
|
||||
const kosync = {
|
||||
...settings.kosync,
|
||||
userkey: '',
|
||||
enabled: false,
|
||||
};
|
||||
const newSettings = { ...settings, kosync };
|
||||
setSettings(newSettings);
|
||||
await saveSettings(envConfig, newSettings);
|
||||
setUsername('');
|
||||
@@ -213,26 +186,23 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleStrategyChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newStrategy = e.target.value as KoreaderSyncStrategy;
|
||||
const newSettings = { ...settings, koreaderSyncStrategy: newStrategy };
|
||||
const kosync = {
|
||||
...settings.kosync,
|
||||
strategy: e.target.value as KOSyncStrategy,
|
||||
};
|
||||
|
||||
const newSettings = { ...settings, kosync };
|
||||
setSettings(newSettings);
|
||||
await saveSettings(envConfig, newSettings);
|
||||
};
|
||||
|
||||
const handleChecksumMethodChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newMethod = e.target.value as KoreaderSyncChecksumMethod;
|
||||
const newSettings = { ...settings, koreaderSyncChecksumMethod: newMethod };
|
||||
setSettings(newSettings);
|
||||
await saveSettings(envConfig, newSettings);
|
||||
};
|
||||
const kosync = {
|
||||
...settings.kosync,
|
||||
checksumMethod: e.target.value as KOSyncChecksumMethod,
|
||||
};
|
||||
|
||||
const handleToleranceChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const sliderValue = parseInt(e.target.value, 10);
|
||||
setToleranceSliderValue(sliderValue);
|
||||
// Calculate the actual tolerance from the slider value (e.g., 4 -> 0.0001)
|
||||
const newTolerance = Math.pow(10, -sliderValue);
|
||||
|
||||
const newSettings = { ...settings, koreaderSyncPercentageTolerance: newTolerance };
|
||||
const newSettings = { ...settings, kosync };
|
||||
setSettings(newSettings);
|
||||
await saveSettings(envConfig, newSettings);
|
||||
};
|
||||
@@ -251,18 +221,16 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
<div className='text-center'>
|
||||
<p className='text-base-content/80 text-sm'>
|
||||
{_('Sync as {{userDisplayName}}', {
|
||||
userDisplayName: settings.koreaderSyncUsername,
|
||||
userDisplayName: settings.kosync.username,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex h-14 items-center justify-between'>
|
||||
<span className='text-base-content/80'>
|
||||
{_('Sync Server Connected', { username: settings.koreaderSyncUsername })}
|
||||
</span>
|
||||
<span className='text-base-content/80'>{_('Sync Server Connected')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={settings.koreaderSyncStrategy !== 'disabled'}
|
||||
checked={settings.kosync.enabled}
|
||||
onChange={() => handleDisconnect()}
|
||||
/>
|
||||
</div>
|
||||
@@ -271,14 +239,13 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
<span className='label-text font-medium'>{_('Sync Strategy')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.koreaderSyncStrategy}
|
||||
value={settings.kosync.strategy}
|
||||
onChange={handleStrategyChange}
|
||||
options={[
|
||||
{ value: 'prompt', label: _('Ask on conflict') },
|
||||
{ value: 'silent', label: _('Always use latest') },
|
||||
{ value: 'send', label: _('Send changes only') },
|
||||
{ value: 'receive', label: _('Receive changes only') },
|
||||
{ value: 'disable', label: _('Disabled') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -287,7 +254,7 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
<span className='label-text font-medium'>{_('Checksum Method')}</span>
|
||||
</label>
|
||||
<StyledSelect
|
||||
value={settings.koreaderSyncChecksumMethod}
|
||||
value={settings.kosync.checksumMethod}
|
||||
onChange={handleChecksumMethodChange}
|
||||
options={[
|
||||
{ value: 'binary', label: _('File Content (recommended)') },
|
||||
@@ -307,27 +274,6 @@ export const KOSyncSettingsWindow: React.FC = () => {
|
||||
onChange={handleDeviceNameChange}
|
||||
/>
|
||||
</div>
|
||||
{/* Hidden to avoid confusing users with technical details */}
|
||||
{false && (
|
||||
<div className='form-control w-full'>
|
||||
<label className='label py-1'>
|
||||
<span className='label-text font-medium'>{_('Sync Tolerance')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='range'
|
||||
min='0'
|
||||
max='15'
|
||||
value={toleranceSliderValue}
|
||||
onChange={handleToleranceChange}
|
||||
className='range range-primary'
|
||||
/>
|
||||
<div className='text-base-content/70 mt-2 text-center text-xs'>
|
||||
{_('Precision: {{precision}} digits after the decimal', {
|
||||
precision: toleranceSliderValue,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -21,6 +21,7 @@ import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { getSysFontsList, setSystemUIVisibility } from '@/utils/bridge';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
import { KOSyncSettingsWindow } from './KOSyncSettings';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import { initDayjs } from '@/utils/time';
|
||||
@@ -120,6 +121,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
<ReaderContent ids={ids} settings={settings} />
|
||||
<AboutWindow />
|
||||
<UpdaterWindow />
|
||||
<KOSyncSettingsWindow />
|
||||
<Toast />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
if (isPrimary && book && config) {
|
||||
const settings = useSettingsStore.getState().settings;
|
||||
eventDispatcher.dispatch('sync-book-progress', { bookKey });
|
||||
eventDispatcher.dispatch('flush-koreader-sync', { bookKey });
|
||||
eventDispatcher.dispatch('flush-kosync', { bookKey });
|
||||
await saveConfig(envConfig, bookKey, config, settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,10 +8,12 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useParallelViewStore } from '@/store/parallelViewStore';
|
||||
import { isWebAppPlatform } from '@/services/environment';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { DOWNLOAD_READEST_URL } from '@/services/constants';
|
||||
import { setKOSyncSettingsWindowVisible } from '@/app/reader/components/KOSyncSettings';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
@@ -22,6 +24,7 @@ interface BookMenuProps {
|
||||
|
||||
const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen }) => {
|
||||
const _ = useTranslation();
|
||||
const { settings } = useSettingsStore();
|
||||
const { bookKeys, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const { getVisibleLibrary } = useLibraryStore();
|
||||
const { openParallelView } = useBooksManager();
|
||||
@@ -69,6 +72,18 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
unsetParallel(bookKeys);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const showKoSyncSettingsWindow = () => {
|
||||
setKOSyncSettingsWindowVisible(true);
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handlePullKOSync = () => {
|
||||
eventDispatcher.dispatch('pull-kosync', { bookKey: sideBarBookKey });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
const handlePushKOSync = () => {
|
||||
eventDispatcher.dispatch('push-kosync', { bookKey: sideBarBookKey });
|
||||
setIsDropdownOpen?.(false);
|
||||
};
|
||||
|
||||
const isWebApp = isWebAppPlatform();
|
||||
|
||||
@@ -118,6 +133,14 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
</ul>
|
||||
</MenuItem>
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem label={_('KOReader Sync')} onClick={showKoSyncSettingsWindow} />
|
||||
{settings.kosync.enabled && (
|
||||
<>
|
||||
<MenuItem label={_('Push Progress')} onClick={handlePushKOSync} />
|
||||
<MenuItem label={_('Pull Progress')} onClick={handlePullKOSync} />
|
||||
</>
|
||||
)}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem label={_('Export Annotations')} onClick={handleExportAnnotations} />
|
||||
<MenuItem
|
||||
label={_('Sort TOC by Page')}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { md5 } from 'js-md5';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { KOSyncClient, KoSyncProgress } from '@/services/sync/KOSyncClient';
|
||||
import { Book, FIXED_LAYOUT_FORMATS } from '@/types/book';
|
||||
import { Book, BookProgress, FIXED_LAYOUT_FORMATS } from '@/types/book';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
@@ -30,386 +28,257 @@ export interface SyncDetails {
|
||||
|
||||
export const useKOSync = (bookKey: string) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { appService } = useEnv();
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
const [kosyncClient, setKOSyncClient] = useState<KOSyncClient | null>(null);
|
||||
const [syncState, setSyncState] = useState<SyncState>('idle');
|
||||
const [conflictDetails, setConflictDetails] = useState<SyncDetails | null>(null);
|
||||
const [errorMessage] = useState<string | null>(null);
|
||||
const hasPulledOnce = useRef(false);
|
||||
|
||||
const syncCompletedForKey = useRef<string | null>(null);
|
||||
const lastPushedCfiRef = useRef<string | null>(null);
|
||||
const bookData = getBookData(bookKey);
|
||||
const book = bookData?.book;
|
||||
const bookDoc = bookData?.bookDoc;
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
useEffect(() => {
|
||||
lastPushedCfiRef.current = null;
|
||||
syncCompletedForKey.current = null;
|
||||
setSyncState('idle');
|
||||
}, [bookKey]);
|
||||
if (!settings.kosync.username || !settings.kosync.userkey) {
|
||||
setKOSyncClient(null);
|
||||
return;
|
||||
}
|
||||
const client = new KOSyncClient(settings.kosync);
|
||||
setKOSyncClient(client);
|
||||
}, [settings]);
|
||||
|
||||
const mapProgressToServerFormat = useCallback(() => {
|
||||
const currentProgress = getProgress(bookKey);
|
||||
const currentBook = getBookData(bookKey)?.book;
|
||||
if (!currentProgress || !currentBook) return null;
|
||||
const generateKOProgress = useCallback(() => {
|
||||
const progress = getProgress(bookKey);
|
||||
const book = getBookData(bookKey)?.book;
|
||||
if (!progress || !book) return null;
|
||||
|
||||
let progressStr: string;
|
||||
let koProgress = '';
|
||||
let percentage: number;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) {
|
||||
const page = (currentProgress.section?.current ?? 0) + 1;
|
||||
const totalPages = currentProgress.section?.total ?? 0;
|
||||
progressStr = page.toString();
|
||||
percentage = totalPages > 0 ? page / totalPages : 0;
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const page = progress.section?.current ?? 0;
|
||||
const totalPages = progress.section?.total ?? 0;
|
||||
koProgress = page.toString();
|
||||
percentage = totalPages > 0 ? (page + 1) / totalPages : 0;
|
||||
} else {
|
||||
progressStr = currentProgress.location;
|
||||
const view = getView(bookKey);
|
||||
|
||||
if (view && progressStr) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index: spineIndex } = content;
|
||||
const converter = new XCFI(doc, spineIndex || 0);
|
||||
const xpointerResult = converter.cfiToXPointer(progressStr);
|
||||
|
||||
progressStr = xpointerResult.xpointer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to convert CFI to XPointer. Progress will be sent as percentage only.',
|
||||
error,
|
||||
);
|
||||
const cfi = progress.location;
|
||||
if (!view || !cfi) return null;
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index: spineIndex } = content;
|
||||
const converter = new XCFI(doc, spineIndex || 0);
|
||||
const xpointerResult = converter.cfiToXPointer(cfi);
|
||||
koProgress = xpointerResult.xpointer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to convert CFI to XPointer', error);
|
||||
}
|
||||
|
||||
const page = currentProgress.pageinfo?.current ?? 0;
|
||||
const totalPages = currentProgress.pageinfo?.total ?? 0;
|
||||
const page = progress.pageinfo?.current ?? 0;
|
||||
const totalPages = progress.pageinfo?.total ?? 0;
|
||||
percentage = totalPages > 0 ? (page + 1) / totalPages : 0;
|
||||
}
|
||||
|
||||
return { progressStr, percentage };
|
||||
return { koProgress, percentage };
|
||||
}, [bookKey, getProgress, getBookData, getView]);
|
||||
|
||||
const applyRemoteProgress = async (book: Book, bookDoc: BookDoc, remote: KoSyncProgress) => {
|
||||
const view = getView(bookKey);
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const pageToGo = parseInt(remote.progress!, 10);
|
||||
if (isNaN(pageToGo)) return;
|
||||
view?.select(pageToGo - 1);
|
||||
} else {
|
||||
if (!remote.progress?.startsWith('/body')) return;
|
||||
try {
|
||||
const content = view?.renderer.getContents()[0];
|
||||
const cfi = await getCFIFromXPointer(
|
||||
remote.progress!,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookDoc,
|
||||
);
|
||||
view?.goTo(cfi);
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', { message: _('Reading Progress Synced'), type: 'info' });
|
||||
};
|
||||
|
||||
const promptedSync = async (
|
||||
book: Book,
|
||||
bookDoc: BookDoc,
|
||||
local: BookProgress,
|
||||
remote: KoSyncProgress,
|
||||
) => {
|
||||
let localPreview = '';
|
||||
let remotePreview = '';
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const localPageInfo = local.section;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = localPageInfo
|
||||
? _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: localPageInfo.current + 1,
|
||||
total: localPageInfo.total,
|
||||
percentage: localPercentage,
|
||||
})
|
||||
: _('Current position');
|
||||
|
||||
const remotePage = parseInt(remote.progress!, 10);
|
||||
if (!isNaN(remotePage) && remotePercentage > 0) {
|
||||
const localTotalPages = localPageInfo?.total ?? 0;
|
||||
const remoteTotalPages = Math.round(remotePage / remotePercentage);
|
||||
const pagesMatch = Math.abs(localTotalPages - remoteTotalPages) <= 1;
|
||||
|
||||
if (pagesMatch) {
|
||||
remotePreview = _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
} else {
|
||||
remotePreview = _('Approximately page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const localPageInfo = local.pageinfo;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = `${local.sectionLabel} (${localPercentage}%)`;
|
||||
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
|
||||
setConflictDetails({
|
||||
book,
|
||||
bookDoc,
|
||||
local: { cfi: local.location, preview: localPreview },
|
||||
remote: { ...remote, preview: remotePreview },
|
||||
});
|
||||
};
|
||||
|
||||
const pushProgress = useMemo(
|
||||
() =>
|
||||
debounce(async () => {
|
||||
const { settings: currentSettings } = useSettingsStore.getState();
|
||||
if (!bookKey || !appService || !kosyncClient || !hasPulledOnce.current) return;
|
||||
const { settings } = useSettingsStore.getState();
|
||||
if (['receive', 'disable'].includes(settings.kosync.strategy)) return;
|
||||
|
||||
const currentBook = getBookData(bookKey)?.book;
|
||||
const progress = generateKOProgress();
|
||||
if (!currentBook || !progress || !progress.koProgress) return;
|
||||
|
||||
const { koreaderSyncUsername, koreaderSyncUserkey, koreaderSyncStrategy } = currentSettings;
|
||||
if (
|
||||
!koreaderSyncUsername ||
|
||||
!koreaderSyncUserkey ||
|
||||
['receive', 'disable'].includes(koreaderSyncStrategy) ||
|
||||
!currentBook
|
||||
)
|
||||
return;
|
||||
|
||||
const getDocumentDigest = (bookToDigest: Book): string => {
|
||||
if (currentSettings.koreaderSyncChecksumMethod === 'filename') {
|
||||
const filename = bookToDigest.sourceTitle || bookToDigest.title;
|
||||
const normalizedPath = filename.replace(/\\/g, '/');
|
||||
return md5(
|
||||
normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || normalizedPath,
|
||||
);
|
||||
}
|
||||
return bookToDigest.hash;
|
||||
};
|
||||
|
||||
const getDeviceName = async () => {
|
||||
if (currentSettings.koreaderSyncDeviceName) return currentSettings.koreaderSyncDeviceName;
|
||||
if (appService?.appPlatform === 'tauri') {
|
||||
const name = await osType();
|
||||
return `Readest (${name.charAt(0).toUpperCase() + name.slice(1)})`;
|
||||
}
|
||||
return 'Readest';
|
||||
};
|
||||
|
||||
const digest = getDocumentDigest(currentBook);
|
||||
const progressData = mapProgressToServerFormat();
|
||||
if (!digest || !progressData) return;
|
||||
|
||||
if (progressData.progressStr === lastPushedCfiRef.current) return;
|
||||
|
||||
const deviceName = await getDeviceName();
|
||||
const client = new KOSyncClient(
|
||||
currentSettings.koreaderSyncServerUrl,
|
||||
currentSettings.koreaderSyncUsername,
|
||||
currentSettings.koreaderSyncUserkey,
|
||||
currentSettings.koreaderSyncChecksumMethod,
|
||||
currentSettings.koreaderSyncDeviceId,
|
||||
deviceName,
|
||||
);
|
||||
|
||||
await client.updateProgress(currentBook, progressData.progressStr, progressData.percentage);
|
||||
lastPushedCfiRef.current = progressData.progressStr;
|
||||
await kosyncClient.updateProgress(currentBook, progress.koProgress, progress.percentage);
|
||||
}, 5000),
|
||||
[bookKey, appService, getBookData, mapProgressToServerFormat],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[bookKey, appService, kosyncClient, generateKOProgress],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
const { bookKey: syncBookKey } = event.detail;
|
||||
if (syncBookKey === bookKey) {
|
||||
pushProgress.flush();
|
||||
}
|
||||
};
|
||||
eventDispatcher.on('flush-koreader-sync', handleFlush);
|
||||
return () => {
|
||||
eventDispatcher.off('flush-koreader-sync', handleFlush);
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [bookKey, pushProgress]);
|
||||
const pullProgress = useCallback(
|
||||
async () => {
|
||||
if (!progress?.location || !appService || !kosyncClient) return;
|
||||
|
||||
useEffect(() => {
|
||||
const performInitialSync = async () => {
|
||||
const { koreaderSyncUsername, koreaderSyncUserkey, koreaderSyncStrategy } = settings;
|
||||
if (
|
||||
!book ||
|
||||
!bookDoc ||
|
||||
!progress ||
|
||||
!koreaderSyncUsername ||
|
||||
!koreaderSyncUserkey ||
|
||||
koreaderSyncStrategy === 'disabled'
|
||||
)
|
||||
return;
|
||||
const bookData = getBookData(bookKey);
|
||||
const book = bookData?.book;
|
||||
const bookDoc = bookData?.bookDoc;
|
||||
if (!book || !bookDoc) return;
|
||||
|
||||
if (koreaderSyncStrategy === 'send') {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const { strategy, enabled } = settings.kosync;
|
||||
if (!enabled) return;
|
||||
|
||||
hasPulledOnce.current = true;
|
||||
if (strategy === 'send') {
|
||||
setSyncState('synced');
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncState('checking');
|
||||
|
||||
const getDeviceName = async () => {
|
||||
if (settings.koreaderSyncDeviceName) return settings.koreaderSyncDeviceName;
|
||||
if (appService?.appPlatform === 'tauri') {
|
||||
const name = await osType();
|
||||
return `Readest (${name.charAt(0).toUpperCase() + name.slice(1)})`;
|
||||
}
|
||||
return 'Readest';
|
||||
};
|
||||
|
||||
const deviceName = await getDeviceName();
|
||||
const client = new KOSyncClient(
|
||||
settings.koreaderSyncServerUrl,
|
||||
settings.koreaderSyncUsername,
|
||||
settings.koreaderSyncUserkey,
|
||||
settings.koreaderSyncChecksumMethod,
|
||||
settings.koreaderSyncDeviceId,
|
||||
deviceName,
|
||||
);
|
||||
const remote = await client.getProgress(book);
|
||||
lastPushedCfiRef.current = progress.location;
|
||||
|
||||
if (!remote?.progress || !remote?.timestamp) {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const remoteProgress = await kosyncClient.getProgress(book);
|
||||
if (!remoteProgress || !remoteProgress.progress || !remoteProgress.timestamp) {
|
||||
setSyncState('synced');
|
||||
if (settings.koreaderSyncStrategy !== 'receive') {
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const localTimestamp = bookData?.config?.updatedAt || book.updatedAt;
|
||||
const remoteIsNewer = remote.timestamp * 1000 > localTimestamp;
|
||||
|
||||
const localIdentifier = FIXED_LAYOUT_FORMATS.has(book.format)
|
||||
? progress.section?.current.toString()
|
||||
: progress.location;
|
||||
const isLocalCFI = localIdentifier?.startsWith('epubcfi');
|
||||
|
||||
const remoteIdentifier = FIXED_LAYOUT_FORMATS.has(book.format)
|
||||
? (parseInt(remote.progress, 10) - 1).toString()
|
||||
: remote.progress.startsWith('epubcfi')
|
||||
? remote.progress
|
||||
: null;
|
||||
const isRemoteCFI = remoteIdentifier?.startsWith('epubcfi');
|
||||
|
||||
let isProgressIdentical = false;
|
||||
if (isLocalCFI && isRemoteCFI) {
|
||||
isProgressIdentical = localIdentifier === remoteIdentifier;
|
||||
}
|
||||
|
||||
if (!isProgressIdentical) {
|
||||
const localPercentage = mapProgressToServerFormat()?.percentage ?? 0;
|
||||
const remotePercentage = remote.percentage;
|
||||
|
||||
if (remotePercentage !== undefined && remotePercentage !== null) {
|
||||
const tolerance = settings.koreaderSyncPercentageTolerance;
|
||||
const percentageDifference = Math.abs(localPercentage - remotePercentage);
|
||||
isProgressIdentical = percentageDifference < tolerance;
|
||||
}
|
||||
}
|
||||
|
||||
if (isProgressIdentical) {
|
||||
lastPushedCfiRef.current = localIdentifier;
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const remoteIsNewer = remoteProgress.timestamp * 1000 > localTimestamp;
|
||||
if (strategy === 'receive' || (strategy === 'silent' && remoteIsNewer)) {
|
||||
applyRemoteProgress(book, bookDoc, remoteProgress);
|
||||
setSyncState('synced');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
settings.koreaderSyncStrategy === 'receive' ||
|
||||
(settings.koreaderSyncStrategy === 'silent' && remoteIsNewer)
|
||||
) {
|
||||
const applyRemoteProgress = async () => {
|
||||
const view = getView(bookKey);
|
||||
if (view && remote.progress) {
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const pageToGo = parseInt(remote.progress, 10);
|
||||
if (!isNaN(pageToGo)) view.select(pageToGo - 1);
|
||||
} else {
|
||||
const isXPointer = remote.progress.startsWith('/body');
|
||||
if (isXPointer) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index } = content;
|
||||
const cfi = await getCFIFromXPointer(remote.progress, doc, index || 0, bookDoc);
|
||||
view.goTo(cfi);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Reading Progress Synced'),
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to convert XPointer to CFI, falling back to percentage.',
|
||||
error,
|
||||
);
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Reading Progress Synced'),
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
applyRemoteProgress();
|
||||
syncCompletedForKey.current = bookKey;
|
||||
setSyncState('synced');
|
||||
} else if (settings.koreaderSyncStrategy === 'prompt') {
|
||||
let localPreview = '';
|
||||
let remotePreview = '';
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const localPageInfo = progress.section;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = localPageInfo
|
||||
? _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: localPageInfo.current + 1,
|
||||
total: localPageInfo.total,
|
||||
percentage: localPercentage,
|
||||
})
|
||||
: _('Current position');
|
||||
|
||||
const remotePage = parseInt(remote.progress, 10);
|
||||
if (!isNaN(remotePage) && remotePercentage > 0) {
|
||||
const localTotalPages = localPageInfo?.total ?? 0;
|
||||
const remoteTotalPages = Math.round(remotePage / remotePercentage);
|
||||
const pagesMatch = Math.abs(localTotalPages - remoteTotalPages) <= 1;
|
||||
|
||||
if (pagesMatch) {
|
||||
remotePreview = _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
} else {
|
||||
remotePreview = _('Approximately page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const localPageInfo = progress.pageinfo;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = `${progress.sectionLabel} (${localPercentage}%)`;
|
||||
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
|
||||
setConflictDetails({
|
||||
book,
|
||||
bookDoc,
|
||||
local: { cfi: progress.location, preview: localPreview },
|
||||
remote: { ...remote, preview: remotePreview, percentage: remote.percentage },
|
||||
});
|
||||
} else if (strategy === 'prompt') {
|
||||
promptedSync(book, bookDoc, progress, remoteProgress);
|
||||
setSyncState('conflict');
|
||||
} else {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
setSyncState('synced');
|
||||
}
|
||||
};
|
||||
|
||||
if (bookKey && book && progress && syncCompletedForKey.current !== bookKey) {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
performInitialSync();
|
||||
}
|
||||
}, [
|
||||
bookKey,
|
||||
book,
|
||||
bookDoc,
|
||||
progress,
|
||||
settings,
|
||||
appService,
|
||||
getBookData,
|
||||
getProgress,
|
||||
getView,
|
||||
mapProgressToServerFormat,
|
||||
pushProgress,
|
||||
_,
|
||||
bookData?.config?.updatedAt,
|
||||
]);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[bookKey, appService, kosyncClient, settings.kosync, progress],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePullProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pullProgress();
|
||||
};
|
||||
const handlePushProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress();
|
||||
};
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress.flush();
|
||||
};
|
||||
eventDispatcher.on('pull-kosync', handlePullProgress);
|
||||
eventDispatcher.on('push-kosync', handlePushProgress);
|
||||
eventDispatcher.on('flush-kosync', handleFlush);
|
||||
return () => {
|
||||
eventDispatcher.off('pull-kosync', handlePullProgress);
|
||||
eventDispatcher.off('push-kosync', handlePushProgress);
|
||||
eventDispatcher.off('flush-kosync', handleFlush);
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [bookKey, pushProgress, pullProgress]);
|
||||
|
||||
// Pull: pull progress once when the book is opened
|
||||
useEffect(() => {
|
||||
if (!appService || !kosyncClient || !progress?.location) return;
|
||||
if (hasPulledOnce.current) return;
|
||||
|
||||
pullProgress();
|
||||
}, [appService, kosyncClient, progress?.location, pushProgress, pullProgress]);
|
||||
|
||||
// Push: auto-push progress when progress changes with a debounce
|
||||
useEffect(() => {
|
||||
if (syncState === 'synced' && progress) {
|
||||
if (
|
||||
settings.koreaderSyncStrategy !== 'receive' &&
|
||||
settings.koreaderSyncStrategy !== 'disabled'
|
||||
) {
|
||||
const { strategy, enabled } = settings.kosync;
|
||||
if (strategy !== 'receive' && enabled) {
|
||||
pushProgress();
|
||||
}
|
||||
}
|
||||
}, [progress, syncState, settings.koreaderSyncStrategy, pushProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [pushProgress]);
|
||||
}, [progress, syncState, settings.kosync, pushProgress]);
|
||||
|
||||
const resolveConflictWithLocal = () => {
|
||||
pushProgress();
|
||||
@@ -421,50 +290,12 @@ export const useKOSync = (bookKey: string) => {
|
||||
const resolveConflictWithRemote = async () => {
|
||||
const view = getView(bookKey);
|
||||
const remote = conflictDetails?.remote;
|
||||
const currentBook = conflictDetails?.book;
|
||||
const book = conflictDetails?.book;
|
||||
const bookDoc = conflictDetails?.bookDoc;
|
||||
|
||||
if (view && remote?.progress && currentBook) {
|
||||
if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) {
|
||||
const localTotalPages = getProgress(bookKey)?.section?.total ?? 0;
|
||||
const remotePage = parseInt(remote.progress, 10);
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
const remoteTotalPages =
|
||||
remotePercentage > 0 ? Math.round(remotePage / remotePercentage) : 0;
|
||||
if (!book || !bookDoc || !remote || !remote.progress || !view) return;
|
||||
|
||||
if (!isNaN(remotePage) && Math.abs(localTotalPages - remoteTotalPages) <= 1) {
|
||||
console.log('Going to remote page:', remotePage);
|
||||
view.select(remotePage - 1);
|
||||
} else if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
console.log('Going to remote percentage:', remote.percentage);
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
} else {
|
||||
const isXPointer = remote.progress.startsWith('/body');
|
||||
const isCFI = remote.progress.startsWith('epubcfi');
|
||||
|
||||
if (isXPointer) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index } = content;
|
||||
const cfi = await getCFIFromXPointer(remote.progress, doc, index || 0, bookDoc);
|
||||
view.goTo(cfi);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI, falling back to percentage.', error);
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
} else if (isCFI) {
|
||||
view.goTo(remote.progress);
|
||||
} else if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', { message: _('Reading Progress Synced'), type: 'info' });
|
||||
}
|
||||
applyRemoteProgress(book, bookDoc, remote);
|
||||
setSyncState('synced');
|
||||
setConflictDetails(null);
|
||||
};
|
||||
@@ -473,8 +304,9 @@ export const useKOSync = (bookKey: string) => {
|
||||
syncState,
|
||||
conflictDetails,
|
||||
errorMessage,
|
||||
pushProgress,
|
||||
pullProgress,
|
||||
resolveConflictWithLocal,
|
||||
resolveConflictWithRemote,
|
||||
pushProgress,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -116,8 +116,8 @@ export abstract class BaseAppService implements AppService {
|
||||
...settings.globalViewSettings,
|
||||
};
|
||||
|
||||
if (!settings.koreaderSyncDeviceId) {
|
||||
settings.koreaderSyncDeviceId = uuidv4();
|
||||
if (!settings.kosync.deviceId) {
|
||||
settings.kosync.deviceId = uuidv4();
|
||||
await this.fs.writeFile(fp, base, JSON.stringify(settings));
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ViewConfig,
|
||||
ViewSettings,
|
||||
} from '@/types/book';
|
||||
import { ReadSettings, SystemSettings } from '@/types/settings';
|
||||
import { KOSyncSettings, ReadSettings, SystemSettings } from '@/types/settings';
|
||||
import { UserStorageQuota, UserDailyTranslationQuota } from '@/types/user';
|
||||
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
@@ -37,6 +37,17 @@ export const BOOK_UNGROUPED_ID = '';
|
||||
export const SUPPORTED_IMAGE_EXTS = ['png', 'jpg', 'jpeg'];
|
||||
export const IMAGE_ACCEPT_FORMATS = SUPPORTED_IMAGE_EXTS.map((ext) => `.${ext}`).join(', ');
|
||||
|
||||
export const DEFAULT_KOSYNC_SETTINGS = {
|
||||
serverUrl: 'https://sync.koreader.rocks/', // https://kosync.ak-team.com:3042/
|
||||
username: '',
|
||||
userkey: '',
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
checksumMethod: 'binary',
|
||||
strategy: 'prompt',
|
||||
enabled: false,
|
||||
} as KOSyncSettings;
|
||||
|
||||
export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
keepLogin: false,
|
||||
autoUpload: true,
|
||||
@@ -54,14 +65,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
librarySortAscending: false,
|
||||
libraryCoverFit: 'crop',
|
||||
|
||||
koreaderSyncServerUrl: 'https://sync.koreader.rocks/', // https://kosync.ak-team.com:3042/
|
||||
koreaderSyncUsername: '',
|
||||
koreaderSyncUserkey: '',
|
||||
koreaderSyncDeviceId: '',
|
||||
koreaderSyncDeviceName: '',
|
||||
koreaderSyncChecksumMethod: 'binary',
|
||||
koreaderSyncStrategy: 'prompt',
|
||||
koreaderSyncPercentageTolerance: 0.00001,
|
||||
kosync: DEFAULT_KOSYNC_SETTINGS,
|
||||
|
||||
lastSyncedAtBooks: 0,
|
||||
lastSyncedAtConfigs: 0,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { md5 } from 'js-md5';
|
||||
import { Book } from '@/types/book';
|
||||
import { KoreaderSyncChecksumMethod } from '@/types/settings';
|
||||
import { KOSyncSettings } from '@/types/settings';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { KoSyncProxyPayload } from '@/types/kosync';
|
||||
import { isLanAddress } from '@/utils/network';
|
||||
import { getBaseFilename } from '@/utils/path';
|
||||
import { getAPIBaseUrl, isTauriAppPlatform } from '../environment';
|
||||
|
||||
/**
|
||||
@@ -20,29 +19,13 @@ export interface KoSyncProgress {
|
||||
}
|
||||
|
||||
export class KOSyncClient {
|
||||
private serverUrl: string;
|
||||
private username: string;
|
||||
private userkey: string;
|
||||
private checksumMethod: KoreaderSyncChecksumMethod;
|
||||
private deviceId: string;
|
||||
private deviceName: string;
|
||||
private config: KOSyncSettings;
|
||||
private isLanServer: boolean;
|
||||
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
username: string,
|
||||
userkey: string,
|
||||
checksumMethod: KoreaderSyncChecksumMethod,
|
||||
deviceId: string,
|
||||
deviceName: string,
|
||||
) {
|
||||
this.serverUrl = serverUrl.replace(/\/$/, '');
|
||||
this.username = username;
|
||||
this.userkey = userkey;
|
||||
this.checksumMethod = checksumMethod;
|
||||
this.deviceId = deviceId;
|
||||
this.deviceName = deviceName;
|
||||
this.isLanServer = isLanAddress(this.serverUrl);
|
||||
constructor(config: KOSyncSettings) {
|
||||
this.config = config;
|
||||
this.config.serverUrl = config.serverUrl.replace(/\/$/, '');
|
||||
this.isLanServer = isLanAddress(this.config.serverUrl);
|
||||
}
|
||||
|
||||
private async request(
|
||||
@@ -58,13 +41,13 @@ export class KOSyncClient {
|
||||
|
||||
const headers = new Headers(additionalHeaders || {});
|
||||
if (useAuth) {
|
||||
headers.set('X-Auth-User', this.username);
|
||||
headers.set('X-Auth-Key', this.userkey);
|
||||
headers.set('X-Auth-User', this.config.username);
|
||||
headers.set('X-Auth-Key', this.config.userkey);
|
||||
}
|
||||
|
||||
if (this.isLanServer) {
|
||||
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
|
||||
const directUrl = `${this.serverUrl}${endpoint}`;
|
||||
const directUrl = `${this.config.serverUrl}${endpoint}`;
|
||||
|
||||
return fetch(directUrl, {
|
||||
method,
|
||||
@@ -83,7 +66,7 @@ export class KOSyncClient {
|
||||
const proxyUrl = `${getAPIBaseUrl()}/kosync`;
|
||||
|
||||
const proxyBody: KoSyncProxyPayload = {
|
||||
serverUrl: this.serverUrl,
|
||||
serverUrl: this.config.serverUrl,
|
||||
endpoint,
|
||||
method,
|
||||
headers: Object.fromEntries(headers.entries()),
|
||||
@@ -159,7 +142,7 @@ export class KOSyncClient {
|
||||
* @returns Promise with the progress data or null if not found
|
||||
*/
|
||||
async getProgress(book: Book): Promise<KoSyncProgress | null> {
|
||||
if (!this.userkey) return null;
|
||||
if (!this.config.userkey) return null;
|
||||
|
||||
const documentHash = this.getDocumentDigest(book);
|
||||
if (!documentHash) return null;
|
||||
@@ -190,17 +173,17 @@ export class KOSyncClient {
|
||||
* @returns Promise with boolean indicating success
|
||||
*/
|
||||
async updateProgress(book: Book, progress: string, percentage: number): Promise<boolean> {
|
||||
if (!this.userkey) return false;
|
||||
if (!this.config.userkey) return false;
|
||||
|
||||
const documentHash = this.getDocumentDigest(book);
|
||||
if (!documentHash) return false;
|
||||
|
||||
const payload = {
|
||||
document: documentHash,
|
||||
progress: progress.toString(),
|
||||
progress,
|
||||
percentage,
|
||||
device: this.deviceName,
|
||||
device_id: this.deviceId,
|
||||
device: this.config.deviceName,
|
||||
device_id: this.config.deviceId,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -222,10 +205,9 @@ export class KOSyncClient {
|
||||
}
|
||||
}
|
||||
|
||||
private getDocumentDigest(book: Book): string | undefined {
|
||||
if (this.checksumMethod === 'filename') {
|
||||
const filename = getBaseFilename(book.sourceTitle || book.title);
|
||||
return md5(filename);
|
||||
getDocumentDigest(book: Book): string {
|
||||
if (this.config.checksumMethod === 'filename') {
|
||||
console.warn('This is not possible anymore, using md5 instead.');
|
||||
}
|
||||
return book.hash;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ export type LibraryViewModeType = 'grid' | 'list';
|
||||
export type LibrarySortByType = 'title' | 'author' | 'updated' | 'created' | 'size' | 'format';
|
||||
export type LibraryCoverFitType = 'crop' | 'fit';
|
||||
|
||||
export type KoreaderSyncChecksumMethod = 'binary' | 'filename';
|
||||
export type KoreaderSyncStrategy = 'prompt' | 'silent' | 'send' | 'receive' | 'disabled';
|
||||
export type KOSyncChecksumMethod = 'binary' | 'filename';
|
||||
export type KOSyncStrategy = 'prompt' | 'silent' | 'send' | 'receive';
|
||||
|
||||
export interface ReadSettings {
|
||||
sideBarWidth: string;
|
||||
@@ -24,6 +24,17 @@ export interface ReadSettings {
|
||||
customThemes: CustomTheme[];
|
||||
}
|
||||
|
||||
export interface KOSyncSettings {
|
||||
enabled: boolean;
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
userkey: string;
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
checksumMethod: KOSyncChecksumMethod;
|
||||
strategy: KOSyncStrategy;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
version: number;
|
||||
localBooksDir: string;
|
||||
@@ -45,14 +56,7 @@ export interface SystemSettings {
|
||||
libraryCoverFit: LibraryCoverFitType;
|
||||
customFonts: CustomFont[];
|
||||
|
||||
koreaderSyncServerUrl: string;
|
||||
koreaderSyncUsername: string;
|
||||
koreaderSyncUserkey: string;
|
||||
koreaderSyncDeviceId: string;
|
||||
koreaderSyncDeviceName: string;
|
||||
koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod;
|
||||
koreaderSyncStrategy: KoreaderSyncStrategy;
|
||||
koreaderSyncPercentageTolerance: number;
|
||||
kosync: KOSyncSettings;
|
||||
|
||||
lastSyncedAtBooks: number;
|
||||
lastSyncedAtConfigs: number;
|
||||
|
||||
Reference in New Issue
Block a user