feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import (#4338)

* feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import

- BooknoteView: show 'No annotation yet' / 'No bookmark yet' when empty
- Notebook: show 'No note yet' when no notes/excerpts exist
- Annotator: add loading overlay with spinner during Moon+ Reader import
- mrexpt: yield to event loop every 5 entries to keep spinner animating
- CustomFonts: show in-place loading card during font import, spinner transitions to font name without layout jump

* fix(ui): respect e-ink overlay styling and drop dead className branch

- Annotator: use modal-box on the mrexpt import overlay so eink picks up
  the no-shadow + 1px border override automatically.
- CustomFonts: collapse importing-card clsx ternary whose branches were
  identical into a flat className.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
loveheaven
2026-05-29 00:37:04 +08:00
committed by GitHub
parent bb81d6270f
commit 3c134380b7
5 changed files with 205 additions and 93 deletions
@@ -125,6 +125,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [editingAnnotation, setEditingAnnotation] = useState<BookNote | null>(null);
const [externalDragPoint, setExternalDragPoint] = useState<Point | null>(null);
const [showExportDialog, setShowExportDialog] = useState(false);
const [importingMrexpt, setImportingMrexpt] = useState(false);
// "Clear Annotations" confirm dialog. Hosted here (and not in BookMenu)
// because the menu unmounts the moment the user picks the entry, which
// would otherwise tear down the dialog state immediately.
@@ -1049,68 +1050,73 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return;
}
let conversion;
setImportingMrexpt(true);
try {
conversion = await convertMrexptEntriesToBookNotes(entries, bookDoc, {
highlightStyle: settings.globalReadSettings.highlightStyle,
highlightColor:
settings.globalReadSettings.highlightStyles[settings.globalReadSettings.highlightStyle],
});
} catch (e) {
console.warn('Failed to convert mrexpt entries:', e);
eventDispatcher.dispatch('toast', {
type: 'warning',
message: _('Failed to import annotations.'),
timeout: 3000,
});
return;
}
let conversion;
try {
conversion = await convertMrexptEntriesToBookNotes(entries, bookDoc, {
highlightStyle: settings.globalReadSettings.highlightStyle,
highlightColor:
settings.globalReadSettings.highlightStyles[settings.globalReadSettings.highlightStyle],
});
} catch (e) {
console.warn('Failed to convert mrexpt entries:', e);
eventDispatcher.dispatch('toast', {
type: 'warning',
message: _('Failed to import annotations.'),
timeout: 3000,
});
return;
}
if (conversion.notes.length === 0) {
if (conversion.notes.length === 0) {
eventDispatcher.dispatch('toast', {
type: 'info',
message: _('No annotations could be located in this book.'),
timeout: 2500,
});
return;
}
// Merge into the current book config, deduplicating by note id and
// preferring the latest updatedAt for any conflicting entries.
const config = getConfig(bookKey)!;
const { merged, applied, added, updated } = mergeImportedBookNotes(
config.booknotes ?? [],
conversion.notes,
);
const updatedConfig = updateBooknotes(bookKey, merged);
if (updatedConfig) {
saveConfig(envConfig, bookKey, updatedConfig, settings);
}
// Apply imported (or resurrected) annotations to all live views so
// they appear immediately. We only re-draw the notes that actually
// changed in this round, otherwise duplicate addAnnotation calls
// can confuse the overlay layer.
const views = getViewsById(bookKey.split('-')[0]!);
for (const note of applied) {
try {
views.forEach((v) => v?.addAnnotation(note));
} catch (err) {
console.warn('Failed to add imported annotation', { note, err });
}
}
// A single result toast: the count if anything changed, otherwise a
// plain "nothing new" hint for a repeated import of the same file.
const imported = added + updated;
eventDispatcher.dispatch('toast', {
type: 'info',
message: _('No annotations could be located in this book.'),
message:
imported > 0
? _('Imported {{count}} annotations', { count: imported })
: _('No new annotations to import'),
timeout: 2500,
});
return;
} finally {
setImportingMrexpt(false);
}
// Merge into the current book config, deduplicating by note id and
// preferring the latest updatedAt for any conflicting entries.
const config = getConfig(bookKey)!;
const { merged, applied, added, updated } = mergeImportedBookNotes(
config.booknotes ?? [],
conversion.notes,
);
const updatedConfig = updateBooknotes(bookKey, merged);
if (updatedConfig) {
saveConfig(envConfig, bookKey, updatedConfig, settings);
}
// Apply imported (or resurrected) annotations to all live views so
// they appear immediately. We only re-draw the notes that actually
// changed in this round, otherwise duplicate addAnnotation calls
// can confuse the overlay layer.
const views = getViewsById(bookKey.split('-')[0]!);
for (const note of applied) {
try {
views.forEach((v) => v?.addAnnotation(note));
} catch (err) {
console.warn('Failed to add imported annotation', { note, err });
}
}
// A single result toast: the count if anything changed, otherwise a
// plain "nothing new" hint for a repeated import of the same file.
const imported = added + updated;
eventDispatcher.dispatch('toast', {
type: 'info',
message:
imported > 0
? _('Imported {{count}} annotations', { count: imported })
: _('No new annotations to import'),
timeout: 2500,
});
};
const handleExportMarkdown = async (event: CustomEvent) => {
@@ -1430,6 +1436,28 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
/>
</ModalPortal>
)}
{importingMrexpt && (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30'>
<div className='modal-box bg-base-100 flex flex-col items-center gap-3 px-8 py-6 shadow-2xl'>
<svg className='text-primary h-8 w-8 animate-spin' viewBox='0 0 24 24' fill='none'>
<circle
className='opacity-25'
cx='12'
cy='12'
r='10'
stroke='currentColor'
strokeWidth='4'
/>
<path
className='opacity-75'
fill='currentColor'
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z'
/>
</svg>
<p className='font-size-sm text-base-content'>{_('Importing annotations...')}</p>
</div>
</div>
)}
</div>
);
};