From 4f55920b7134946fbfabb89ade04dea0ab2d55e3 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 27 Apr 2026 23:04:29 +0800 Subject: [PATCH] feat(kosync): add metadata hash info dialog to diagnose sync failures (#3978) --- .../public/locales/ar/translation.json | 7 ++- .../public/locales/bn/translation.json | 7 ++- .../public/locales/bo/translation.json | 7 ++- .../public/locales/de/translation.json | 7 ++- .../public/locales/el/translation.json | 7 ++- .../public/locales/es/translation.json | 7 ++- .../public/locales/fa/translation.json | 7 ++- .../public/locales/fr/translation.json | 7 ++- .../public/locales/he/translation.json | 7 ++- .../public/locales/hi/translation.json | 7 ++- .../public/locales/hu/translation.json | 7 ++- .../public/locales/id/translation.json | 7 ++- .../public/locales/it/translation.json | 7 ++- .../public/locales/ja/translation.json | 7 ++- .../public/locales/ko/translation.json | 7 ++- .../public/locales/ms/translation.json | 7 ++- .../public/locales/nl/translation.json | 7 ++- .../public/locales/pl/translation.json | 7 ++- .../public/locales/pt/translation.json | 7 ++- .../public/locales/ro/translation.json | 7 ++- .../public/locales/ru/translation.json | 7 ++- .../public/locales/si/translation.json | 7 ++- .../public/locales/sl/translation.json | 7 ++- .../public/locales/sv/translation.json | 7 ++- .../public/locales/ta/translation.json | 7 ++- .../public/locales/th/translation.json | 7 ++- .../public/locales/tr/translation.json | 7 ++- .../public/locales/uk/translation.json | 7 ++- .../public/locales/vi/translation.json | 7 ++- .../public/locales/zh-CN/translation.json | 7 ++- .../public/locales/zh-TW/translation.json | 7 ++- .../utils/metadata-hash-info.test.ts | 52 ++++++++++++++++ .../src/app/reader/components/HeaderBar.tsx | 21 ++++++- .../reader/components/MetaHashInfoDialog.tsx | 61 +++++++++++++++++++ .../src/app/reader/components/ViewMenu.tsx | 22 ++++++- apps/readest-app/src/utils/book.ts | 22 +++++-- apps/readest.koplugin/main.lua | 47 ++++++++++++++ apps/readest.koplugin/syncconfig.lua | 48 +++++++++------ 38 files changed, 433 insertions(+), 57 deletions(-) create mode 100644 apps/readest-app/src/__tests__/utils/metadata-hash-info.test.ts create mode 100644 apps/readest-app/src/app/reader/components/MetaHashInfoDialog.tsx diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index e16b3d05..6bd2d96f 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1277,5 +1277,10 @@ "{{count}} failed_two": "{{count}} فشل", "{{count}} failed_few": "{{count}} فشل", "{{count}} failed_many": "{{count}} فشل", - "{{count}} failed_other": "{{count}} فشل" + "{{count}} failed_other": "{{count}} فشل", + "(none)": "(لا شيء)", + "Metadata Hash": "تجزئة البيانات الوصفية", + "Meta Hash": "تجزئة البيانات الوصفية", + "Computed Hash": "التجزئة المحسوبة", + "Identifiers": "المعرفات" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 88d7a629..7f0ad514 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "সব এড়িয়ে যান", "Retry all": "সব পুনরায় চেষ্টা করুন", "{{count}} failed_one": "{{count}}টি ব্যর্থ", - "{{count}} failed_other": "{{count}}টি ব্যর্থ" + "{{count}} failed_other": "{{count}}টি ব্যর্থ", + "(none)": "(কোনোটি না)", + "Metadata Hash": "মেটাডেটা হ্যাশ", + "Meta Hash": "মেটা হ্যাশ", + "Computed Hash": "গণিত হ্যাশ", + "Identifiers": "শনাক্তকারী" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index ef694605..9e681745 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1192,5 +1192,10 @@ "Skip": "གཡོལ།", "Skip all": "ཡོད་ཚད་གཡོལ།", "Retry all": "ཡོད་ཚད་ཡང་སྐྱར།", - "{{count}} failed_other": "{{count}} ཕམ་ཁ།" + "{{count}} failed_other": "{{count}} ཕམ་ཁ།", + "(none)": "(མེད)", + "Metadata Hash": "དཔྱད་གཞིའི་ཧེཤི", + "Meta Hash": "དཔྱད་གཞིའི་ཧེཤི", + "Computed Hash": "རྩིས་པའི་ཧེཤི", + "Identifiers": "ངོས་འཛིན་བྱེད་མཁན" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index b20b4f53..4cda1dd3 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Alle überspringen", "Retry all": "Alle erneut versuchen", "{{count}} failed_one": "{{count}} fehlgeschlagen", - "{{count}} failed_other": "{{count}} fehlgeschlagen" + "{{count}} failed_other": "{{count}} fehlgeschlagen", + "(none)": "(keine)", + "Metadata Hash": "Metadaten-Hash", + "Meta Hash": "Meta-Hash", + "Computed Hash": "Berechneter Hash", + "Identifiers": "Bezeichner" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index fd96c73e..40893a01 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Παράλειψη όλων", "Retry all": "Επανάληψη όλων", "{{count}} failed_one": "{{count}} απέτυχε", - "{{count}} failed_other": "{{count}} απέτυχαν" + "{{count}} failed_other": "{{count}} απέτυχαν", + "(none)": "(κανένα)", + "Metadata Hash": "Hash Μεταδεδομένων", + "Meta Hash": "Meta Hash", + "Computed Hash": "Υπολογισμένο Hash", + "Identifiers": "Αναγνωριστικά" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index afb403dd..3c8063b6 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "Reintentar todos", "{{count}} failed_one": "{{count}} fallida", "{{count}} failed_many": "{{count}} fallidas", - "{{count}} failed_other": "{{count}} fallidas" + "{{count}} failed_other": "{{count}} fallidas", + "(none)": "(ninguno)", + "Metadata Hash": "Hash de Metadatos", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hash Calculado", + "Identifiers": "Identificadores" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 94f48bec..68fb3950 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "رد کردن همه", "Retry all": "تلاش مجدد همه", "{{count}} failed_one": "{{count}} ناموفق", - "{{count}} failed_other": "{{count}} ناموفق" + "{{count}} failed_other": "{{count}} ناموفق", + "(none)": "(هیچ‌کدام)", + "Metadata Hash": "هش متادیتا", + "Meta Hash": "متا هش", + "Computed Hash": "هش محاسبه‌شده", + "Identifiers": "شناسه‌ها" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 11be4127..40b7c271 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "Tout réessayer", "{{count}} failed_one": "{{count}} échec", "{{count}} failed_many": "{{count}} échecs", - "{{count}} failed_other": "{{count}} échecs" + "{{count}} failed_other": "{{count}} échecs", + "(none)": "(aucun)", + "Metadata Hash": "Hachage des métadonnées", + "Meta Hash": "Hachage Meta", + "Computed Hash": "Hachage calculé", + "Identifiers": "Identifiants" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index d3a256c4..cc5f617d 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "נסה שוב את הכל", "{{count}} failed_one": "{{count}} נכשל", "{{count}} failed_two": "{{count}} נכשלו", - "{{count}} failed_other": "{{count}} נכשלו" + "{{count}} failed_other": "{{count}} נכשלו", + "(none)": "(אין)", + "Metadata Hash": "גיבוב מטא-נתונים", + "Meta Hash": "גיבוב מטא", + "Computed Hash": "גיבוב מחושב", + "Identifiers": "מזהים" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 24a9e5da..0943b26a 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "सब छोड़ें", "Retry all": "सब पुनः प्रयास करें", "{{count}} failed_one": "{{count}} असफल", - "{{count}} failed_other": "{{count}} असफल" + "{{count}} failed_other": "{{count}} असफल", + "(none)": "(कोई नहीं)", + "Metadata Hash": "मेटाडेटा हैश", + "Meta Hash": "मेटा हैश", + "Computed Hash": "परिकलित हैश", + "Identifiers": "पहचानकर्ता" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 6f85e36b..73be627e 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Összes kihagyása", "Retry all": "Összes újrapróbálása", "{{count}} failed_one": "{{count}} sikertelen", - "{{count}} failed_other": "{{count}} sikertelen" + "{{count}} failed_other": "{{count}} sikertelen", + "(none)": "(nincs)", + "Metadata Hash": "Metaadat-hash", + "Meta Hash": "Meta-hash", + "Computed Hash": "Számított hash", + "Identifiers": "Azonosítók" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index d0184801..f4106fd6 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1192,5 +1192,10 @@ "Skip": "Lewati", "Skip all": "Lewati semua", "Retry all": "Coba lagi semua", - "{{count}} failed_other": "{{count}} gagal" + "{{count}} failed_other": "{{count}} gagal", + "(none)": "(tidak ada)", + "Metadata Hash": "Hash Metadata", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hash Terhitung", + "Identifiers": "Pengenal" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 9e8bf756..03cf7a10 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "Riprova tutti", "{{count}} failed_one": "{{count}} non riuscito", "{{count}} failed_many": "{{count}} non riusciti", - "{{count}} failed_other": "{{count}} non riusciti" + "{{count}} failed_other": "{{count}} non riusciti", + "(none)": "(nessuno)", + "Metadata Hash": "Hash Metadati", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hash Calcolato", + "Identifiers": "Identificatori" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 64ed2d14..c0cdb61e 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1192,5 +1192,10 @@ "Skip": "スキップ", "Skip all": "すべてスキップ", "Retry all": "すべて再試行", - "{{count}} failed_other": "{{count}}件失敗" + "{{count}} failed_other": "{{count}}件失敗", + "(none)": "(なし)", + "Metadata Hash": "メタデータハッシュ", + "Meta Hash": "メタハッシュ", + "Computed Hash": "計算されたハッシュ", + "Identifiers": "識別子" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 6b73a114..f18ea5f5 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1192,5 +1192,10 @@ "Skip": "건너뛰기", "Skip all": "모두 건너뛰기", "Retry all": "모두 재시도", - "{{count}} failed_other": "{{count}}개 실패" + "{{count}} failed_other": "{{count}}개 실패", + "(none)": "(없음)", + "Metadata Hash": "메타데이터 해시", + "Meta Hash": "메타 해시", + "Computed Hash": "계산된 해시", + "Identifiers": "식별자" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 3e9cfe99..8807358d 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1192,5 +1192,10 @@ "Skip": "Langkau", "Skip all": "Langkau semua", "Retry all": "Cuba lagi semua", - "{{count}} failed_other": "{{count}} gagal" + "{{count}} failed_other": "{{count}} gagal", + "(none)": "(tiada)", + "Metadata Hash": "Cincang Metadata", + "Meta Hash": "Cincang Meta", + "Computed Hash": "Cincang Dikira", + "Identifiers": "Pengenal" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index fa3d596b..31b0744e 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Alles overslaan", "Retry all": "Alles opnieuw proberen", "{{count}} failed_one": "{{count}} mislukt", - "{{count}} failed_other": "{{count}} mislukt" + "{{count}} failed_other": "{{count}} mislukt", + "(none)": "(geen)", + "Metadata Hash": "Metadata-hash", + "Meta Hash": "Meta-hash", + "Computed Hash": "Berekende hash", + "Identifiers": "Identificatoren" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index a486b1c2..43f3ac1f 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1243,5 +1243,10 @@ "{{count}} failed_one": "{{count}} nieudane", "{{count}} failed_few": "{{count}} nieudane", "{{count}} failed_many": "{{count}} nieudanych", - "{{count}} failed_other": "{{count}} nieudanych" + "{{count}} failed_other": "{{count}} nieudanych", + "(none)": "(brak)", + "Metadata Hash": "Skrót metadanych", + "Meta Hash": "Meta-skrót", + "Computed Hash": "Obliczony skrót", + "Identifiers": "Identyfikatory" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index b34cef7a..53f682ac 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "Tentar tudo novamente", "{{count}} failed_one": "{{count}} falhada", "{{count}} failed_many": "{{count}} falhadas", - "{{count}} failed_other": "{{count}} falhadas" + "{{count}} failed_other": "{{count}} falhadas", + "(none)": "(nenhum)", + "Metadata Hash": "Hash de Metadados", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hash Calculado", + "Identifiers": "Identificadores" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 2da8db71..55a57b12 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1226,5 +1226,10 @@ "Retry all": "Reîncearcă toate", "{{count}} failed_one": "{{count}} eșuat", "{{count}} failed_few": "{{count}} eșuate", - "{{count}} failed_other": "{{count}} eșuate" + "{{count}} failed_other": "{{count}} eșuate", + "(none)": "(niciunul)", + "Metadata Hash": "Hash metadate", + "Meta Hash": "Meta hash", + "Computed Hash": "Hash calculat", + "Identifiers": "Identificatori" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 5ead6732..40c03742 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1243,5 +1243,10 @@ "{{count}} failed_one": "{{count}} ошибка", "{{count}} failed_few": "{{count}} ошибки", "{{count}} failed_many": "{{count}} ошибок", - "{{count}} failed_other": "{{count}} ошибок" + "{{count}} failed_other": "{{count}} ошибок", + "(none)": "(нет)", + "Metadata Hash": "Хеш метаданных", + "Meta Hash": "Мета-хеш", + "Computed Hash": "Вычисленный хеш", + "Identifiers": "Идентификаторы" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 77eef874..595f09fd 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "සියල්ල මඟ හරින්න", "Retry all": "සියල්ල නැවත උත්සාහ කරන්න", "{{count}} failed_one": "{{count}} අසාර්ථකයි", - "{{count}} failed_other": "{{count}} අසාර්ථකයි" + "{{count}} failed_other": "{{count}} අසාර්ථකයි", + "(none)": "(කිසිවක් නැත)", + "Metadata Hash": "පාරදත්ත හෑෂ්", + "Meta Hash": "මෙටා හෑෂ්", + "Computed Hash": "ගණනය කළ හෑෂ්", + "Identifiers": "හඳුනාගැනීම්" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 7e265679..a540b282 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1243,5 +1243,10 @@ "{{count}} failed_one": "{{count}} ni uspela", "{{count}} failed_two": "{{count}} nista uspeli", "{{count}} failed_few": "{{count}} niso uspele", - "{{count}} failed_other": "{{count}} jih ni uspelo" + "{{count}} failed_other": "{{count}} jih ni uspelo", + "(none)": "(brez)", + "Metadata Hash": "Hash metapodatkov", + "Meta Hash": "Meta hash", + "Computed Hash": "Izračunani hash", + "Identifiers": "Identifikatorji" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 88513da1..882870d6 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Hoppa över alla", "Retry all": "Försök igen för alla", "{{count}} failed_one": "{{count}} misslyckades", - "{{count}} failed_other": "{{count}} misslyckades" + "{{count}} failed_other": "{{count}} misslyckades", + "(none)": "(ingen)", + "Metadata Hash": "Metadata-hash", + "Meta Hash": "Meta-hash", + "Computed Hash": "Beräknat hash", + "Identifiers": "Identifierare" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 368fb259..292c6dfc 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "அனைத்தையும் தவிர்", "Retry all": "அனைத்தையும் மீண்டும் முயற்சி செய்", "{{count}} failed_one": "{{count}} தோல்வி", - "{{count}} failed_other": "{{count}} தோல்விகள்" + "{{count}} failed_other": "{{count}} தோல்விகள்", + "(none)": "(இல்லை)", + "Metadata Hash": "மெட்டாடேட்டா ஹாஷ்", + "Meta Hash": "மெட்டா ஹாஷ்", + "Computed Hash": "கணக்கிடப்பட்ட ஹாஷ்", + "Identifiers": "அடையாளங்காட்டிகள்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 21c6d4bf..27219212 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1192,5 +1192,10 @@ "Skip": "ข้าม", "Skip all": "ข้ามทั้งหมด", "Retry all": "ลองอีกครั้งทั้งหมด", - "{{count}} failed_other": "{{count}} ล้มเหลว" + "{{count}} failed_other": "{{count}} ล้มเหลว", + "(none)": "(ไม่มี)", + "Metadata Hash": "แฮชเมตาดาต้า", + "Meta Hash": "แฮชเมตา", + "Computed Hash": "แฮชที่คำนวณ", + "Identifiers": "ตัวระบุ" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index c4dc6763..5b0544fd 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1209,5 +1209,10 @@ "Skip all": "Tümünü atla", "Retry all": "Tümünü yeniden dene", "{{count}} failed_one": "{{count}} başarısız", - "{{count}} failed_other": "{{count}} başarısız" + "{{count}} failed_other": "{{count}} başarısız", + "(none)": "(yok)", + "Metadata Hash": "Üst Veri Hash'i", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hesaplanan Hash", + "Identifiers": "Tanımlayıcılar" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index da7e8ee6..8bd5ef8d 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1243,5 +1243,10 @@ "{{count}} failed_one": "{{count}} помилка", "{{count}} failed_few": "{{count}} помилки", "{{count}} failed_many": "{{count}} помилок", - "{{count}} failed_other": "{{count}} помилок" + "{{count}} failed_other": "{{count}} помилок", + "(none)": "(немає)", + "Metadata Hash": "Хеш метаданих", + "Meta Hash": "Мета-хеш", + "Computed Hash": "Обчислений хеш", + "Identifiers": "Ідентифікатори" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 8834e7f3..95621b1b 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1192,5 +1192,10 @@ "Skip": "Bỏ qua", "Skip all": "Bỏ qua tất cả", "Retry all": "Thử lại tất cả", - "{{count}} failed_other": "{{count}} thất bại" + "{{count}} failed_other": "{{count}} thất bại", + "(none)": "(không có)", + "Metadata Hash": "Hash Siêu Dữ Liệu", + "Meta Hash": "Meta Hash", + "Computed Hash": "Hash Đã Tính", + "Identifiers": "Định Danh" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index a0c6d1d5..29726882 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1192,5 +1192,10 @@ "Skip": "跳过", "Skip all": "全部跳过", "Retry all": "全部重试", - "{{count}} failed_other": "{{count}} 个失败" + "{{count}} failed_other": "{{count}} 个失败", + "(none)": "(无)", + "Metadata Hash": "元数据哈希", + "Meta Hash": "元哈希", + "Computed Hash": "计算的哈希", + "Identifiers": "标识符" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index ed8d8945..3253cf61 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1192,5 +1192,10 @@ "Skip": "跳過", "Skip all": "全部跳過", "Retry all": "全部重試", - "{{count}} failed_other": "{{count}} 個失敗" + "{{count}} failed_other": "{{count}} 個失敗", + "(none)": "(無)", + "Metadata Hash": "中繼資料雜湊", + "Meta Hash": "中繼雜湊", + "Computed Hash": "計算所得雜湊", + "Identifiers": "識別碼" } diff --git a/apps/readest-app/src/__tests__/utils/metadata-hash-info.test.ts b/apps/readest-app/src/__tests__/utils/metadata-hash-info.test.ts new file mode 100644 index 00000000..45deca8e --- /dev/null +++ b/apps/readest-app/src/__tests__/utils/metadata-hash-info.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { getMetadataHash, getMetadataHashInfo } from '@/utils/book'; +import type { BookMetadata } from '@/libs/document'; + +describe('getMetadataHashInfo', () => { + it('returns hash plus the inputs used to compute it', () => { + const metadata: BookMetadata = { + title: 'The Great Gatsby', + author: 'F. Scott Fitzgerald', + language: 'en', + identifier: 'urn:isbn:9780743273565', + }; + + const info = getMetadataHashInfo(metadata); + + expect(info).toBeDefined(); + expect(info!.title).toBe('The Great Gatsby'); + expect(info!.authors).toEqual(['F. Scott Fitzgerald']); + expect(info!.identifiers).toEqual(['9780743273565']); + expect(info!.metaHash).toBe(getMetadataHash(metadata)); + expect(info!.hashSource).toBe('The Great Gatsby|F. Scott Fitzgerald|9780743273565'); + }); + + it('prefers altIdentifier over identifier', () => { + const info = getMetadataHashInfo({ + title: 'Book', + author: 'Author', + language: 'en', + identifier: 'urn:isbn:1234567890', + altIdentifier: 'uuid:abc-123', + }); + + expect(info!.identifiers).toEqual(['abc-123']); + }); + + it('handles LanguageMap titles and Contributor authors', () => { + const info = getMetadataHashInfo({ + title: { en: 'Hello', ja: 'こんにちは' }, + author: [{ name: { en: 'Alice' } }, { name: { en: 'Bob' } }], + language: 'en', + } as unknown as BookMetadata); + + expect(info!.title).toBe('Hello'); + expect(info!.authors).toEqual(['Alice', 'Bob']); + expect(info!.identifiers).toEqual([]); + }); + + it('returns undefined when metadata is missing required fields', () => { + const info = getMetadataHashInfo(null as unknown as BookMetadata); + expect(info).toBeUndefined(); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/HeaderBar.tsx b/apps/readest-app/src/app/reader/components/HeaderBar.tsx index f0046c19..8f714dd1 100644 --- a/apps/readest-app/src/app/reader/components/HeaderBar.tsx +++ b/apps/readest-app/src/app/reader/components/HeaderBar.tsx @@ -7,6 +7,7 @@ import { Insets } from '@/types/misc'; import { useEnv } from '@/context/EnvContext'; import { useThemeStore } from '@/store/themeStore'; import { useReaderStore } from '@/store/readerStore'; +import { useBookDataStore } from '@/store/bookDataStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; @@ -20,6 +21,7 @@ import { AnnotationToolType } from '@/types/annotator'; import { saveViewSettings } from '@/helpers/settings'; import { HighlighterIcon } from '@/components/HighlighterIcon'; import Dropdown from '@/components/Dropdown'; +import ModalPortal from '@/components/ModalPortal'; import WindowButtons from '@/components/WindowButtons'; import QuickActionMenu from './annotator/QuickActionMenu'; import SidebarToggler from './SidebarToggler'; @@ -28,6 +30,7 @@ import NotebookToggler from './NotebookToggler'; import SettingsToggler from './SettingsToggler'; import TranslationToggler from './TranslationToggler'; import ViewMenu from './ViewMenu'; +import MetaHashInfoDialog from './MetaHashInfoDialog'; interface HeaderBarProps { bookKey: string; @@ -61,9 +64,12 @@ const HeaderBar: React.FC = ({ const { isDarkMode, systemUIVisible, statusBarHeight } = useThemeStore(); const { isSideBarVisible, getIsSideBarVisible } = useSidebarStore(); const { getView, getViewSettings, setHoveredBookKey } = useReaderStore(); + const { getBookData } = useBookDataStore(); const viewSettings = getViewSettings(bookKey); + const bookData = getBookData(bookKey); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isMetaHashDialogOpen, setIsMetaHashDialogOpen] = useState(false); const [headerWidth, setHeaderWidth] = useState(0); const view = getView(bookKey); const iconSize16 = useResponsiveSize(16); @@ -282,8 +288,21 @@ const HeaderBar: React.FC = ({ toggleButton={} onToggle={handleToggleDropdown} > - + setIsMetaHashDialogOpen(true)} + /> + {isMetaHashDialogOpen && ( + + setIsMetaHashDialogOpen(false)} + /> + + )} void; +} + +const Row: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +
+ {label} +
+ {value} +
+
+); + +const MetaHashInfoDialog: React.FC = ({ + isOpen, + metadata, + storedMetaHash, + onClose, +}) => { + const _ = useTranslation(); + const info = metadata ? getMetadataHashInfo(metadata) : undefined; + const displayHash = storedMetaHash || info?.metaHash || ''; + const hashesMatch = !info || !storedMetaHash || storedMetaHash === info.metaHash; + const placeholder = _('(none)'); + + return ( + + {isOpen && ( +
+ + {!hashesMatch && info && } + + 0 ? info.authors.join(', ') : placeholder} + /> + 0 ? info.identifiers.join(', ') : placeholder} + /> +
+ )} +
+ ); +}; + +export default MetaHashInfoDialog; diff --git a/apps/readest-app/src/app/reader/components/ViewMenu.tsx b/apps/readest-app/src/app/reader/components/ViewMenu.tsx index 16bac779..2f967204 100644 --- a/apps/readest-app/src/app/reader/components/ViewMenu.tsx +++ b/apps/readest-app/src/app/reader/components/ViewMenu.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { BiMoon, BiSun } from 'react-icons/bi'; import { TbSunMoon } from 'react-icons/tb'; -import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md'; +import { MdZoomOut, MdZoomIn, MdCheck, MdInfoOutline } from 'react-icons/md'; import { MdSync, MdSyncProblem } from 'react-icons/md'; import { IoMdExpand } from 'react-icons/io'; import { TbArrowAutofitWidth } from 'react-icons/tb'; @@ -31,9 +31,14 @@ import Menu from '@/components/Menu'; interface ViewMenuProps { bookKey: string; setIsDropdownOpen?: (open: boolean) => void; + onShowMetaHashDialog?: () => void; } -const ViewMenu: React.FC = ({ bookKey, setIsDropdownOpen }) => { +const ViewMenu: React.FC = ({ + bookKey, + setIsDropdownOpen, + onShowMetaHashDialog, +}) => { const _ = useTranslation(); const router = useRouter(); const { user } = useAuth(); @@ -319,6 +324,19 @@ const ViewMenu: React.FC = ({ bookKey, setIsDropdownOpen }) => { Icon={user ? MdSync : MdSyncProblem} iconClassName={user && viewState?.syncing ? 'animate-reverse-spin' : ''} onClick={handleSync} + siblings={ + + } /> diff --git a/apps/readest-app/src/utils/book.ts b/apps/readest-app/src/utils/book.ts index b9d781da..3c8ef58e 100644 --- a/apps/readest-app/src/utils/book.ts +++ b/apps/readest-app/src/utils/book.ts @@ -308,16 +308,28 @@ const getIdentifiersList = ( : [identifiers.value]; }; -export const getMetadataHash = (metadata: BookMetadata) => { +export interface MetadataHashInfo { + title: string; + authors: string[]; + identifiers: string[]; + hashSource: string; + metaHash: string; +} + +export const getMetadataHashInfo = (metadata: BookMetadata): MetadataHashInfo | undefined => { try { const title = getTitleForHash(metadata.title); - const authors = getAuthorsList(metadata.author).join(','); - const identifiers = getIdentifiersList(metadata.altIdentifier || metadata.identifier).join(','); - const hashSource = `${title}|${authors}|${identifiers}`; + const authors = getAuthorsList(metadata.author); + const identifiers = getIdentifiersList(metadata.altIdentifier || metadata.identifier); + const hashSource = `${title}|${authors.join(',')}|${identifiers.join(',')}`; const metaHash = md5(hashSource.normalize('NFC')); - return metaHash; + return { title, authors, identifiers, hashSource, metaHash }; } catch (error) { console.error('Error generating metadata hash:', error); } return; }; + +export const getMetadataHash = (metadata: BookMetadata) => { + return getMetadataHashInfo(metadata)?.metaHash; +}; diff --git a/apps/readest.koplugin/main.lua b/apps/readest.koplugin/main.lua index a9e56d5e..0dac4f5e 100644 --- a/apps/readest.koplugin/main.lua +++ b/apps/readest.koplugin/main.lua @@ -1,5 +1,6 @@ local Dispatcher = require("dispatcher") local InfoMessage = require("ui/widget/infomessage") +local KeyValuePage = require("ui/widget/keyvaluepage") local WidgetContainer = require("ui/widget/container/widgetcontainer") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") @@ -148,6 +149,16 @@ function ReadestSync:addToMainMenu(menu_items) end, separator = true, }, + { + text = _("Metadata hash info"), + enabled_func = function() + return self.ui.document ~= nil + end, + callback = function() + self:showMetaHashInfo() + end, + separator = true, + }, { text_func = function() if self.installed_version then @@ -197,6 +208,42 @@ function ReadestSync:getBookIdentifiers() return book_hash, meta_hash end +function ReadestSync:showMetaHashInfo() + if not self.ui.document then + UIManager:show(InfoMessage:new{ + text = _("No book is open"), + timeout = 2, + }) + return + end + + local info = SyncConfig:getMetadataHashInfo(self.ui) + local doc_readest_sync = self.ui.doc_settings:readSetting("readest_sync") or {} + local stored_meta_hash = doc_readest_sync.meta_hash_v1 + local placeholder = _("(none)") + + local kv_pairs = { + { _("Meta Hash"), stored_meta_hash or info.meta_hash }, + } + if stored_meta_hash and stored_meta_hash ~= info.meta_hash then + table.insert(kv_pairs, { _("Computed Hash"), info.meta_hash }) + end + table.insert(kv_pairs, { _("Title"), info.title ~= "" and info.title or placeholder }) + table.insert(kv_pairs, { + _("Author"), + #info.authors > 0 and table.concat(info.authors, ", ") or placeholder, + }) + table.insert(kv_pairs, { + _("Identifiers"), + #info.identifiers > 0 and table.concat(info.identifiers, ", ") or placeholder, + }) + + UIManager:show(KeyValuePage:new{ + title = _("Metadata Hash"), + kv_pairs = kv_pairs, + }) +end + -- ── Config sync ──────────────────────────────────────────────────── function ReadestSync:pushBookConfig(interactive) diff --git a/apps/readest.koplugin/syncconfig.lua b/apps/readest.koplugin/syncconfig.lua index e55bc45d..b094098f 100644 --- a/apps/readest.koplugin/syncconfig.lua +++ b/apps/readest.koplugin/syncconfig.lua @@ -22,7 +22,7 @@ local function normalizeAuthor(author) return author end -function SyncConfig:generateMetadataHash(ui) +function SyncConfig:getMetadataHashInfo(ui) local doc_props = ui.doc_settings:readSetting("doc_props") or {} local title = doc_props.title or '' if title == '' then @@ -31,20 +31,21 @@ function SyncConfig:generateMetadataHash(ui) title = basename or '' end - local authors = doc_props.authors or '' - if authors:find("\n") then - authors = util.splitToArray(authors, "\n") - for i, author in ipairs(authors) do - authors[i] = normalizeAuthor(author) + local authors_raw = doc_props.authors or '' + local authors_list = {} + if authors_raw:find("\n") then + local list = util.splitToArray(authors_raw, "\n") + for i, author in ipairs(list) do + authors_list[i] = normalizeAuthor(author) end - authors = table.concat(authors, ",") - else - authors = normalizeAuthor(authors) + elseif authors_raw ~= '' then + authors_list = { normalizeAuthor(authors_raw) } end - local identifiers = doc_props.identifiers or '' - if identifiers:find("\n") then - local list = util.splitToArray(identifiers, "\n") + local identifiers_raw = doc_props.identifiers or '' + local identifiers_list = {} + if identifiers_raw:find("\n") then + local list = util.splitToArray(identifiers_raw, "\n") local normalized = {} local priorities = { "uuid", "calibre", "isbn" } local preferred = nil @@ -59,15 +60,26 @@ function SyncConfig:generateMetadataHash(ui) end end if preferred then - identifiers = preferred + identifiers_list = { preferred } else - identifiers = table.concat(normalized, ",") + identifiers_list = normalized end - else - identifiers = normalizeIdentifier(identifiers) + elseif identifiers_raw ~= '' then + identifiers_list = { normalizeIdentifier(identifiers_raw) } end - local doc_meta = title .. "|" .. authors .. "|" .. identifiers - return sha2.md5(doc_meta) + + local hash_source = title .. "|" .. table.concat(authors_list, ",") .. "|" .. table.concat(identifiers_list, ",") + return { + title = title, + authors = authors_list, + identifiers = identifiers_list, + hash_source = hash_source, + meta_hash = sha2.md5(hash_source), + } +end + +function SyncConfig:generateMetadataHash(ui) + return self:getMetadataHashInfo(ui).meta_hash end function SyncConfig:getMetaHash(ui)