feat(kosync): add metadata hash info dialog to diagnose sync failures (#3978)

This commit is contained in:
Huang Xin
2026-04-27 23:04:29 +08:00
committed by GitHub
parent 63b0b87028
commit 4f55920b71
38 changed files with 433 additions and 57 deletions
@@ -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' />
+17 -5
View File
@@ -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;
};
+47
View File
@@ -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)
+30 -18
View File
@@ -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)