feat(kosync): add metadata hash info dialog to diagnose sync failures (#3978)
This commit is contained in:
@@ -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": "المعرفات"
|
||||
}
|
||||
|
||||
@@ -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": "শনাক্তকারী"
|
||||
}
|
||||
|
||||
@@ -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": "ངོས་འཛིན་བྱེད་མཁན"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Αναγνωριστικά"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "شناسهها"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "מזהים"
|
||||
}
|
||||
|
||||
@@ -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": "पहचानकर्ता"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "識別子"
|
||||
}
|
||||
|
||||
@@ -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": "식별자"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Идентификаторы"
|
||||
}
|
||||
|
||||
@@ -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": "හඳුනාගැනීම්"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "அடையாளங்காட்டிகள்"
|
||||
}
|
||||
|
||||
@@ -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": "ตัวระบุ"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Ідентифікатори"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "标识符"
|
||||
}
|
||||
|
||||
@@ -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": "識別碼"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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<HeaderBarProps> = ({
|
||||
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<HeaderBarProps> = ({
|
||||
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} />
|
||||
<ViewMenu
|
||||
bookKey={bookKey}
|
||||
onShowMetaHashDialog={() => setIsMetaHashDialogOpen(true)}
|
||||
/>
|
||||
</Dropdown>
|
||||
{isMetaHashDialogOpen && (
|
||||
<ModalPortal showOverlay={false}>
|
||||
<MetaHashInfoDialog
|
||||
isOpen={isMetaHashDialogOpen}
|
||||
metadata={bookData?.bookDoc?.metadata ?? bookData?.book?.metadata}
|
||||
storedMetaHash={bookData?.book?.metaHash}
|
||||
onClose={() => setIsMetaHashDialogOpen(false)}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
<WindowButtons
|
||||
className='window-buttons flex items-center'
|
||||
headerRef={headerRef}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { getMetadataHashInfo } from '@/utils/book';
|
||||
|
||||
interface MetaHashInfoDialogProps {
|
||||
isOpen: boolean;
|
||||
metadata: BookMetadata | null | undefined;
|
||||
storedMetaHash?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Row: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<span className='text-base-content/60 text-xs uppercase tracking-wide'>{label}</span>
|
||||
<div className='bg-base-200 text-base-content/90 break-all rounded-md p-2 font-mono text-xs'>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MetaHashInfoDialog: React.FC<MetaHashInfoDialogProps> = ({
|
||||
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 (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={_('Metadata Hash')}
|
||||
boxClassName='sm:!min-w-[520px] sm:h-auto'
|
||||
>
|
||||
{isOpen && (
|
||||
<div className='mb-4 mt-0 flex flex-col gap-3 p-2 sm:p-4'>
|
||||
<Row label={_('Meta Hash')} value={displayHash || placeholder} />
|
||||
{!hashesMatch && info && <Row label={_('Computed Hash')} value={info.metaHash} />}
|
||||
<Row label={_('Title')} value={info?.title || placeholder} />
|
||||
<Row
|
||||
label={_('Author')}
|
||||
value={info && info.authors.length > 0 ? info.authors.join(', ') : placeholder}
|
||||
/>
|
||||
<Row
|
||||
label={_('Identifiers')}
|
||||
value={info && info.identifiers.length > 0 ? info.identifiers.join(', ') : placeholder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetaHashInfoDialog;
|
||||
@@ -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<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
bookKey,
|
||||
setIsDropdownOpen,
|
||||
onShowMetaHashDialog,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
@@ -319,6 +324,19 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
Icon={user ? MdSync : MdSyncProblem}
|
||||
iconClassName={user && viewState?.syncing ? 'animate-reverse-spin' : ''}
|
||||
onClick={handleSync}
|
||||
siblings={
|
||||
<button
|
||||
aria-label={_('Metadata Hash')}
|
||||
title={_('Metadata Hash')}
|
||||
className='hover:bg-base-300 text-base-content/70 mx-1 rounded-md px-2'
|
||||
onClick={() => {
|
||||
setIsDropdownOpen?.(false);
|
||||
onShowMetaHashDialog?.();
|
||||
}}
|
||||
>
|
||||
<MdInfoOutline size={16} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr aria-hidden='true' className='border-base-300 my-1' />
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user