feat(eink): optimize color and layout for e-ink mode (#2887)

This commit is contained in:
Huang Xin
2026-01-08 17:29:33 +01:00
committed by GitHub
parent 93228c4b2a
commit 462ca46fee
47 changed files with 532 additions and 179 deletions
@@ -185,7 +185,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
size={iconSize16}
tipColor={annotationQuickAction === null ? '#8F8F8F' : highlightHexColor}
tipStyle={{
opacity: annotationQuickAction === null ? 0.3 : 0.5,
opacity: annotationQuickAction === null ? 0.5 : 0.8,
mixBlendMode: isDarkMode ? 'screen' : 'multiply',
}}
/>
@@ -147,7 +147,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
<Menu
className={clsx(
'view-menu dropdown-content dropdown-right no-triangle z-20 mt-1 border',
'bgcolor-base-200 border-base-200 shadow-2xl',
'bgcolor-base-200 shadow-2xl',
)}
style={{
maxWidth: `${window.innerWidth - 40}px`,
@@ -98,7 +98,9 @@ const AnnotationNotes: React.FC<AnnotationNotesProps> = ({
role='none'
key={note.id || index}
onClick={() => handleShowAnnotation?.(note)}
className={clsx('cursor-pointer rounded-lg bg-gray-600 shadow-lg transition-colors')}
className={clsx(
'popup-container cursor-pointer rounded-lg bg-gray-600 shadow-lg transition-colors',
)}
style={
isVertical
? {
@@ -113,7 +115,8 @@ const AnnotationNotes: React.FC<AnnotationNotesProps> = ({
<div
dir='auto'
className={clsx(
'm-4 hyphens-auto text-justify font-sans text-sm text-white',
'm-4 hyphens-auto text-justify font-sans text-sm',
'not-eink:text-white eink:text-base-content',
isVertical && 'writing-vertical-rl',
)}
style={
@@ -127,7 +130,7 @@ const AnnotationNotes: React.FC<AnnotationNotesProps> = ({
>
<div className={clsx('flex flex-col justify-between gap-2')}>
{note.note}
<span className='text-sm text-white/50 sm:text-xs'>
<span className='not-eink:text-white/50 eink:text-base-content/50 text-sm sm:text-xs'>
{dayjs(note.createdAt).fromNow()}
</span>
</div>
@@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { BookNote, HighlightColor } from '@/types/book';
import { Point, TextSelection } from '@/utils/sel';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useAnnotationEditor } from '../../hooks/useAnnotationEditor';
@@ -143,6 +144,9 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
onStartEdit,
}) => {
const { settings } = useSettingsStore();
const { isDarkMode } = useThemeStore();
const isEink = settings.globalViewSettings.isEink;
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
const { handlePositions, getHandlePositionsFromRange, handleAnnotationRangeChange } =
useAnnotationEditor({ bookKey, annotation, getAnnotationText, setSelection });
@@ -223,7 +227,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
position={currentStart}
isVertical={isVertical}
type='start'
color={handleColorHex}
color={isEink ? einkFgColor : handleColorHex}
onDragStart={handleStartDragStart}
onDrag={handleStartDrag}
onDragEnd={handleDragEnd}
@@ -232,7 +236,7 @@ const AnnotationRangeEditor: React.FC<AnnotationRangeEditorProps> = ({
position={currentEnd}
isVertical={isVertical}
type='end'
color={handleColorHex}
color={isEink ? einkFgColor : handleColorHex}
onDragStart={handleEndDragStart}
onDrag={handleEndDrag}
onDragEnd={handleDragEnd}
@@ -30,7 +30,9 @@ const AnnotationToolButton: React.FC<AnnotationToolButtonProps> = ({
onClick={handleClick}
className={clsx(
'flex h-8 min-h-8 w-8 items-center justify-center p-0',
disabled ? 'cursor-not-allowed opacity-50' : 'rounded-md hover:bg-gray-500',
disabled
? 'cursor-not-allowed opacity-50'
: 'not-eink:hover:bg-gray-500 eink:hover:border rounded-md',
)}
disabled={disabled}
>
@@ -8,6 +8,7 @@ import { BookNote, BooknoteGroup, HighlightColor, HighlightStyle } from '@/types
import { NOTE_PREFIX } from '@/types/view';
import { NativeTouchEventType } from '@/types/system';
import { getLocale, getOSPlatform, uniqueId } from '@/utils/misc';
import { useThemeStore } from '@/store/themeStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
@@ -42,6 +43,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { isDarkMode } = useThemeStore();
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore();
const { setNotebookVisible, setNotebookNewAnnotation } = useNotebookStore();
@@ -293,10 +295,13 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const onDrawAnnotation = (event: Event) => {
const viewSettings = getViewSettings(bookKey)!;
const isEink = viewSettings.isEink;
const detail = (event as CustomEvent).detail;
const { draw, annotation, doc, range } = detail;
const { style, color } = annotation as BookNote;
const hexColor = getHighlightColorHex(settings, color);
const einkBgColor = isDarkMode ? '#000000' : '#ffffff';
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
if (annotation.note) {
const { defaultView } = doc;
const node = range.startContainer;
@@ -304,7 +309,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { writingMode } = defaultView.getComputedStyle(el);
draw(Overlayer.bubble, { writingMode });
} else if (style === 'highlight') {
draw(Overlayer.highlight, { color: hexColor });
draw(Overlayer.highlight, { color: isEink ? einkBgColor : hexColor });
} else if (['underline', 'squiggly'].includes(style as string)) {
const { defaultView } = doc;
const node = range.startContainer;
@@ -318,7 +323,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const padding = viewSettings.vertical
? (lineHeightValue - fontSizeValue) / 2 - strokeWidth + verticalCompensation
: (lineHeightValue - fontSizeValue) / 2 - strokeWidth + horizontalCompensation;
draw(Overlayer[style as keyof typeof Overlayer], { writingMode, color: hexColor, padding });
draw(Overlayer[style as keyof typeof Overlayer], {
writingMode,
color: isEink ? einkFgColor : hexColor,
padding,
});
}
};
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
import { FaCheckCircle } from 'react-icons/fa';
import { HighlightColor, HighlightStyle } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { saveSysSettings } from '@/helpers/settings';
@@ -34,12 +35,15 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
}) => {
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { isDarkMode } = useThemeStore();
const globalReadSettings = settings.globalReadSettings;
const isEink = settings.globalViewSettings.isEink;
const einkBgColor = isDarkMode ? '#000000' : '#ffffff';
const einkFgColor = isDarkMode ? '#ffffff' : '#000000';
const customColors = globalReadSettings.customHighlightColors;
const [selectedStyle, setSelectedStyle] = useState<HighlightStyle>(_selectedStyle);
const [selectedColor, setSelectedColor] = useState<HighlightColor>(_selectedColor);
const size16 = useResponsiveSize(16);
const size18 = useResponsiveSize(18);
const size28 = useResponsiveSize(28);
const highlightOptionsHeightPx = useResponsiveSize(OPTIONS_HEIGHT_PIX);
const highlightOptionsPaddingPx = useResponsiveSize(OPTIONS_PADDING_PIX);
@@ -93,16 +97,17 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
<button
key={style}
onClick={() => handleSelectStyle(style)}
className='flex items-center justify-center rounded-full bg-gray-700 p-0'
className='not-eink:bg-gray-700 eink-bordered flex items-center justify-center rounded-full p-0'
style={{ width: size28, height: size28, minHeight: size28 }}
>
<div
style={{
width: size16,
height: style === 'squiggly' ? size18 : size16,
height: size16,
...(style === 'highlight' &&
selectedStyle === 'highlight' && {
backgroundColor: customColors[selectedColor],
backgroundColor: isEink ? einkFgColor : customColors[selectedColor],
color: isEink ? einkBgColor : '#d1d5db',
paddingTop: '2px',
}),
...(style === 'highlight' &&
@@ -111,11 +116,15 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
paddingTop: '2px',
}),
...((style === 'underline' || style === 'squiggly') && {
color: '#d1d5db',
color: isEink ? einkFgColor : '#d1d5db',
textDecoration: 'underline',
textDecorationThickness: '2px',
textDecorationColor:
selectedStyle === style ? customColors[selectedColor] : '#d1d5db',
selectedStyle === style
? isEink
? einkFgColor
: customColors[selectedColor]
: '#d1d5db',
...(style === 'squiggly' && { textDecorationStyle: 'wavy' }),
}),
}}
@@ -129,27 +138,32 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
<div
className={clsx(
'flex items-center justify-center gap-2 rounded-3xl bg-gray-700',
'not-eink:bg-gray-700 eink-bordered flex items-center justify-center gap-2 rounded-3xl',
isVertical ? 'flex-col py-2' : 'flex-row px-2',
)}
style={isVertical ? { width: size28 } : { height: size28 }}
>
{colors.map((color) => (
<button
key={color}
onClick={() => handleSelectColor(color)}
style={{
width: size16,
height: size16,
backgroundColor: selectedColor !== color ? customColors[color] : 'transparent',
}}
className='rounded-full p-0'
>
{selectedColor === color && (
<FaCheckCircle size={size16} style={{ fill: customColors[color] }} />
)}
</button>
))}
{colors
.filter((c) => (isEink ? selectedColor === c : true))
.map((color) => (
<button
key={color}
onClick={() => handleSelectColor(color)}
style={{
width: size16,
height: size16,
backgroundColor: selectedColor !== color ? customColors[color] : 'transparent',
}}
className='rounded-full p-0'
>
{selectedColor === color && (
<FaCheckCircle
size={size16}
style={{ fill: isEink ? einkFgColor : customColors[color] }}
/>
)}
</button>
))}
</div>
</div>
);
@@ -43,7 +43,7 @@ const QuickActionMenu: React.FC<QuickActionMenuProps> = ({
<Menu
className={clsx(
'annotation-quick-action-menu dropdown-content z-20 mt-1 border',
'bgcolor-base-200 border-base-200 shadow-2xl',
'bgcolor-base-200 shadow-2xl',
)}
style={{
maxWidth: `${window.innerWidth - 40}px`,
@@ -99,7 +99,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
const canSave = Boolean(note.trim());
return (
<div className='content note-editor-container bg-base-100 mt-2 rounded-md p-2'>
<div className='content booknote-item note-editor-container bg-base-100 mt-2 rounded-md p-2'>
<div className='flex w-full'>
<TextEditor
ref={editorRef}
@@ -313,7 +313,7 @@ const Notebook: React.FC = ({}) => {
handleEditNote(item, true);
}
}}
className='collapse-arrow border-base-300 bg-base-100 collapse border'
className='booknote-item collapse-arrow border-base-300 bg-base-100 collapse border'
>
<div
className={clsx(
@@ -94,7 +94,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
return (
<Menu
className={clsx('book-menu dropdown-content border-base-100 z-20 shadow-2xl', menuClassName)}
className={clsx('book-menu dropdown-content z-20 shadow-2xl', menuClassName)}
onCancel={() => setIsDropdownOpen?.(false)}
>
<MenuItem
@@ -133,7 +133,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, onClick }) =
role='button'
ref={viewRef}
className={clsx(
'border-base-300 content group relative my-2 cursor-pointer rounded-lg p-2',
'booknote-item border-base-300 content group relative my-2 cursor-pointer rounded-lg p-2',
isCurrent
? 'bg-base-300/85 hover:bg-base-300 focus:bg-base-300'
: 'hover:bg-base-300/55 focus:bg-base-300/55 bg-base-100',
@@ -177,7 +177,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, onClick }) =
<div className={clsx('content font-size-sm line-clamp-3', item.note && 'mt-2')}>
<span
className={clsx(
'inline leading-normal',
'booknote-text inline leading-normal',
item.note && 'content font-size-xs text-gray-500',
(item.style === 'underline' || item.style === 'squiggly') &&
'underline decoration-2',
@@ -65,7 +65,7 @@ const SidebarHeader: React.FC<{
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
)}
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-1'}
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu className='fill-base-content' />}
>
@@ -37,9 +37,10 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
const { settings } = useSettingsStore();
const { getBookData } = useBookDataStore();
const { getConfig, setConfig, saveConfig } = useBookDataStore();
const { getView, getProgress } = useReaderStore();
const { getView, getProgress, getViewSettings } = useReaderStore();
const { setSearchTerm, setSearchResults, setSearchProgress } = useSidebarStore();
const { getSearchNavState, getSearchStatus, setSearchStatus } = useSidebarStore();
const viewSettings = getViewSettings(bookKey);
const searchNavState = getSearchNavState(bookKey);
const { searchTerm } = searchNavState;
@@ -327,7 +328,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
return (
<div className='relative flex flex-col gap-3 p-2'>
<div className='bg-base-100 flex h-8 items-center rounded-lg'>
<div className='pl-3'>
<div className='absolute ps-3'>
<FaSearch size={iconSize16} className='text-base-content/50' />
</div>
@@ -338,31 +339,40 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
spellCheck={false}
onChange={handleInputChange}
placeholder={_('Search...')}
className='w-full bg-transparent p-2 pr-0 font-sans text-sm font-light focus:outline-none'
className='search-input w-full bg-transparent p-2 pr-0 ps-10 font-sans text-sm font-light focus:outline-none'
/>
{searchTerm && (
<button
onClick={handleClearInput}
className='flex h-8 w-8 items-center justify-center bg-transparent pe-2'
className='absolute end-8 flex h-8 w-8 items-center justify-center bg-transparent'
aria-label={_('Clear search')}
>
<IoMdCloseCircle size={iconSize16} className='text-base-content/75' />
</button>
)}
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
<div
className={clsx(
'absolute end-2 flex h-8 w-8 items-center rounded-r-lg',
viewSettings?.isEink ? 'bg-transparent' : 'bg-base-300',
)}
>
<Dropdown
label={_('Search Options')}
className={clsx(
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
)}
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3.5'}
buttonClassName={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg',
viewSettings?.isEink ? '!bg-transparent hover:!bg-transparent' : '',
)}
toggleButton={<FaChevronDown size={iconSize12} className='text-base-content/50' />}
>
<SearchOptions
isEink={!!viewSettings?.isEink}
searchConfig={config.searchConfig as BookSearchConfig}
onSearchConfigChanged={handleSearchConfigChange}
/>
@@ -373,7 +383,10 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
{searchHistory.length > 0 && !searchTerm && (
<div className='relative flex'>
<div
className='from-base-200 pointer-events-none absolute left-0 top-0 h-full w-3 bg-gradient-to-r to-transparent'
className={clsx(
'from-base-200 pointer-events-none absolute left-0 top-0 h-full w-3 bg-gradient-to-r to-transparent',
viewSettings?.isEink ? 'hidden' : '',
)}
aria-hidden='true'
/>
<div
@@ -384,20 +397,23 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
<button
key={index}
onClick={() => handleHistoryClick(term)}
className='hover:bg-base-content/20 text-base-content/70 bg-base-100 max-w-[60%] flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
className='hover:bg-base-200/20 text-base-content/70 bg-base-100 max-w-[60%] flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
>
<p className='truncate'>{term}</p>
</button>
))}
</div>
<div
className='from-base-200 pointer-events-none absolute right-6 top-0 h-full w-6 bg-gradient-to-l to-transparent'
className={clsx(
'from-base-200 pointer-events-none absolute right-6 top-0 h-full w-6 bg-gradient-to-l to-transparent',
viewSettings?.isEink ? 'hidden' : '',
)}
aria-hidden='true'
/>
<button
onClick={handleClearHistory}
className={clsx(
'text-base-content/50 hover:text-base-content/80 bg-base-200 flex-shrink-0 items-center',
'text-base-content/50 hover:text-base-content/80 flex-shrink-0 items-center',
'flex h-6 min-h-6 w-8 min-w-8 items-center justify-center p-0',
)}
title={_('Clear search history')}
@@ -6,6 +6,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
interface SearchOptionsProps {
isEink: boolean;
searchConfig: BookSearchConfig;
menuClassName?: string;
onSearchConfigChanged: (searchConfig: BookSearchConfig) => void;
@@ -33,6 +34,7 @@ const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
);
const SearchOptions: React.FC<SearchOptionsProps> = ({
isEink,
searchConfig,
menuClassName,
onSearchConfigChanged,
@@ -47,7 +49,8 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
return (
<div
className={clsx(
'book-menu dropdown-content dropdown-center border-base-200 z-20 w-56 border shadow-2xl',
'search-options dropdown-content dropdown-center border-base-200 z-20 w-56 border shadow-2xl',
isEink ? 'bordercolor-content border-base-content !bg-base-100 border' : '',
menuClassName,
)}
>