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
@@ -0,0 +1,122 @@
use std::process::Command;
/// Known e-ink device manufacturers and brands (case-insensitive matching)
const EINK_MANUFACTURERS: &[&str] = &[
"onyx", // BOOX devices
"boox", // BOOX devices (alternate)
"amazon", // Kindle devices
"kobo", // Kobo e-readers
"remarkable", // reMarkable tablets
"pocketbook", // PocketBook e-readers
"boyue", // Boyue/Likebook devices
"likebook", // Likebook devices
"dasung", // Dasung e-ink monitors
"bigme", // Bigme e-readers
"hisense", // Hisense e-ink phones (A5, A7, etc.)
"hanvon", // Hanvon e-readers
"tolino", // Tolino e-readers
"bookeen", // Bookeen e-readers
"supernote", // Supernote devices
"mobiscribe", // Mobiscribe e-readers
"xiaomi", // Xiaomi InkPalm (needs model check)
"meebook", // Meebook e-readers
];
/// Known e-ink device models (for manufacturers that also make non-e-ink devices)
const EINK_MODELS: &[&str] = &[
"kindle",
"a5pro",
"a7cc", // Hisense e-ink models
"a7e",
"a9",
"inkpalm", // Xiaomi InkPalm
"eink",
"e-ink",
"paper",
"note air",
"note2",
"note3",
"note5",
"nova",
"poke",
"leaf",
"page",
"tab ultra",
"max lumi",
];
/// Get Android system property using getprop command
fn get_system_property(prop: &str) -> Option<String> {
Command::new("getprop")
.arg(prop)
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
if value.is_empty() {
None
} else {
Some(value)
}
} else {
None
}
})
}
/// Check if the current Android device is an e-ink device
pub fn is_eink_device() -> bool {
// Get device manufacturer and model
let manufacturer = get_system_property("ro.product.manufacturer")
.or_else(|| get_system_property("ro.product.brand"))
.unwrap_or_default()
.to_lowercase();
let model = get_system_property("ro.product.model")
.or_else(|| get_system_property("ro.product.device"))
.unwrap_or_default()
.to_lowercase();
let device = get_system_property("ro.product.device")
.unwrap_or_default()
.to_lowercase();
// Check if manufacturer matches known e-ink manufacturers
for eink_manufacturer in EINK_MANUFACTURERS {
if manufacturer.contains(eink_manufacturer) {
// Special case for manufacturers that make both e-ink and non-e-ink devices
if *eink_manufacturer == "hisense" || *eink_manufacturer == "xiaomi" {
// Need to also check the model for these manufacturers
for eink_model in EINK_MODELS {
if model.contains(eink_model) || device.contains(eink_model) {
return true;
}
}
} else {
return true;
}
}
}
// Check if model matches known e-ink models
for eink_model in EINK_MODELS {
if model.contains(eink_model) || device.contains(eink_model) {
return true;
}
}
// Check for e-ink specific system properties
if let Some(eink_support) = get_system_property("ro.eink.support") {
if eink_support == "1" || eink_support.to_lowercase() == "true" {
return true;
}
}
// Check for BOOX specific property
if get_system_property("ro.onyx.devicename").is_some() {
return true;
}
false
}
@@ -0,0 +1,3 @@
pub mod eink;
pub use eink::is_eink_device;
+48 -25
View File
@@ -9,6 +9,9 @@ extern crate objc;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "android")]
mod android;
use tauri::utils::config::BackgroundThrottlingPolicy;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
@@ -270,34 +273,54 @@ pub fn run() {
eprintln!("Failed to initialize tauri_plugin_log: {e}");
};
// Check for e-ink device on Android before building the window
#[cfg(target_os = "android")]
let is_eink = android::is_eink_device();
#[cfg(not(target_os = "android"))]
let is_eink = false;
let eink_script = if is_eink {
"window.__READEST_IS_EINK = true;"
} else {
""
};
let init_script = format!(
r#"
{eink_script}
window.addEventListener('DOMContentLoaded', function() {{
document.documentElement.classList.add('edge-to-edge');
const isTauriLocal = window.location.protocol === 'tauri:' ||
window.location.protocol === 'about:' ||
window.location.hostname === 'tauri.localhost';
const needsSafeArea = !isTauriLocal;
if (needsSafeArea && !document.getElementById('safe-area-style')) {{
const style = document.createElement('style');
style.id = 'safe-area-style';
style.textContent = `
body {{
padding-top: env(safe-area-inset-top) !important;
padding-bottom: env(safe-area-inset-bottom) !important;
padding-left: env(safe-area-inset-left) !important;
padding-right: env(safe-area-inset-right) !important;
}}
`;
document.head.appendChild(style);
}}
}});
"#,
eink_script = eink_script
);
let app_handle = app.handle().clone();
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.background_throttling(BackgroundThrottlingPolicy::Disabled)
.background_color(tauri::window::Color(50, 49, 48, 255))
.initialization_script(
r#"
window.addEventListener('DOMContentLoaded', function() {
document.documentElement.classList.add('edge-to-edge');
const isTauriLocal = window.location.protocol === 'tauri:' ||
window.location.protocol === 'about:' ||
window.location.hostname === 'tauri.localhost';
const needsSafeArea = !isTauriLocal;
if (needsSafeArea && !document.getElementById('safe-area-style')) {
const style = document.createElement('style');
style.id = 'safe-area-style';
style.textContent = `
body {
padding-top: env(safe-area-inset-top) !important;
padding-bottom: env(safe-area-inset-bottom) !important;
padding-left: env(safe-area-inset-left) !important;
padding-right: env(safe-area-inset-right) !important;
}
`;
document.head.appendChild(style);
}
});
"#,
)
.background_color(if is_eink {
tauri::window::Color(255, 255, 255, 255)
} else {
tauri::window::Color(50, 49, 48, 255)
})
.initialization_script(&init_script)
.on_navigation(move |url| {
if url.scheme() == "alipays" || url.scheme() == "alipay" {
let url_str = url.as_str().to_string();
@@ -61,7 +61,7 @@ const BookItem: React.FC<BookItemProps> = ({
>
<div
className={clsx(
'relative flex aspect-[28/41] justify-center rounded',
'bookitem-main relative flex aspect-[28/41] justify-center rounded',
coverFit === 'crop' && 'overflow-hidden shadow-md',
mode === 'grid' && 'items-end',
mode === 'list' && 'min-w-20 items-center',
@@ -337,7 +337,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<button
aria-label={_('Import Books')}
className={clsx(
'border-1 bg-base-100 hover:bg-base-300/50',
'bookitem-main bg-base-100 hover:bg-base-300/50',
'flex items-center justify-center',
'aspect-[28/41] w-full',
)}
@@ -390,7 +390,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
)}
{showDeleteAlert && (
<div
className={clsx('fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
className={clsx('delete-alert fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
style={{
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
}}
@@ -363,7 +363,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-2 sm:px-4 sm:py-4',
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
appService?.isMobileApp && 'no-context-menu',
pressing && mode === 'grid' ? 'scale-95' : 'scale-100',
pressing && mode === 'grid' ? 'not-eink:scale-95' : 'scale-100',
)}
role='button'
tabIndex={0}
@@ -99,7 +99,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
<div className={clsx('group-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}>
<div
className={clsx(
'relative flex overflow-hidden rounded',
'groupitem-main relative flex overflow-hidden rounded',
mode === 'grid' && 'bg-base-100 aspect-[28/41] items-center justify-center shadow-md',
mode === 'list' && 'items-center justify-start gap-4 py-2',
)}
@@ -12,7 +12,6 @@ import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTrafficLight } from '@/hooks/useTrafficLight';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { debounce } from '@/utils/debounce';
@@ -48,13 +47,11 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { settings } = useSettingsStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const { currentBookshelf } = useLibraryStore();
const { isTrafficLightVisible } = useTrafficLight();
const [searchQuery, setSearchQuery] = useState(searchParams?.get('q') ?? '');
const viewSettings = settings.globalViewSettings;
const headerRef = useRef<HTMLDivElement>(null);
const iconSize18 = useResponsiveSize(18);
const { safeAreaInsets: insets } = useThemeStore();
@@ -110,7 +107,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
<div className='relative flex h-9 w-full items-center sm:h-7'>
<span className='text-base-content/50 absolute left-3'>
<span className='text-base-content/50 absolute ps-3'>
<FaSearch className='h-4 w-4' />
</span>
<input
@@ -126,10 +123,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
onChange={handleSearchChange}
spellCheck='false'
className={clsx(
'input rounded-badge h-9 w-full pl-10 pr-[30%] sm:h-7',
viewSettings?.isEink
? 'border-1 border-base-content focus:border-base-content'
: 'bg-base-300/45 border-none',
'search-input input h-9 w-full rounded-full pr-[30%] ps-10 sm:h-7',
'bg-base-300/45 border-0',
'font-sans text-sm font-light',
'placeholder:text-base-content/50 truncate',
'focus:outline-none focus:ring-0',
@@ -17,12 +17,9 @@ export function CatalogDialog({ onClose }: CatalogDialogProps) {
bgClassName={'sm:!bg-black/75'}
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:!max-w-screen-sm'
>
<div className={clsx('bg-base-100 relative flex flex-col overflow-y-auto')}>
<div className={clsx('bg-base-100 relative flex flex-col overflow-y-auto pb-4')}>
<CatalogManager />
</div>
<form method='dialog' className='modal-backdrop'>
<button onClick={onClose}>{_('Close')}</button>
</form>
</Dialog>
);
}
@@ -41,8 +41,8 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
>
<div
className={clsx(
'flex items-center justify-center shadow-lg',
'bg-base-300 text-base-content text-xs',
'text-base-content flex items-center justify-center text-xs shadow-lg',
'not-eink:bg-base-300 eink:bg-base-100 eink:border eink:border-base-content',
'mx-auto w-fit space-x-6 rounded-lg p-4',
)}
>
@@ -232,7 +232,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
return (
<Menu
className={clsx(
'settings-menu dropdown-content no-triangle border-base-100',
'settings-menu dropdown-content no-triangle',
'z-20 mt-2 max-w-[90vw] shadow-2xl',
)}
onCancel={() => setIsDropdownOpen?.(false)}
@@ -99,7 +99,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
return (
<Menu
className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
className='view-menu dropdown-content no-triangle z-20 mt-2 shadow-2xl'
onCancel={() => setIsDropdownOpen?.(false)}
>
{viewOptions.map((option) => (
@@ -406,9 +406,6 @@ export function CatalogManager() {
</div>
</form>
</div>
<form method='dialog' className='modal-backdrop'>
<button onClick={() => setShowAddDialog(false)}>{_('Close')}</button>
</form>
</dialog>
</ModalPortal>
)}
@@ -163,19 +163,13 @@ export function PublicationView({
<Dropdown
label={_('Download')}
className='dropdown-bottom flex justify-center'
buttonClassName='btn btn-ghost p-0 hover:bg-transparent'
buttonClassName={clsx(
'btn btn-ghost btn-primary min-w-20 rounded-3xl p-0 hover:bg-transparent',
downloadedBook && 'btn-success',
)}
disabled={downloading}
toggleButton={
<div
role='button'
tabIndex={0}
className={clsx(
`btn btn-primary min-w-20 rounded-3xl ${downloading ? 'btn-disabled' : ''}`,
downloadedBook && 'btn-success',
)}
>
{downloadedBook ? _('Open') : getAcquisitionLabel(rel)}
</div>
<div>{downloadedBook ? _('Open') : getAcquisitionLabel(rel)}</div>
}
>
<div
@@ -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,
)}
>
@@ -101,7 +101,7 @@ export const AboutWindow = () => {
<div className='my-1 h-5'>
{!updateStatus && (
<button
className='badge badge-primary cursor-pointer p-2'
className='btn btn-sm btn-primary cursor-pointer p-1 text-xs'
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
>
{_('Check Update')}
+1 -1
View File
@@ -191,7 +191,7 @@ const Dialog: React.FC<DialogProps> = ({
>
<Overlay
className={clsx(
'z-10 bg-black/50 sm:bg-black/50',
'dialog-overlay z-10 bg-black/50 sm:bg-black/50',
appService?.hasRoundedWindow && 'rounded-window',
bgClassName,
)}
+4 -1
View File
@@ -29,7 +29,10 @@ const Menu: React.FC<MenuProps> = ({ children, className, style, onCancel }) =>
<div
ref={menuRef}
role='none'
className={clsx('max-h-[calc(100vh-96px)] overflow-y-auto', className)}
className={clsx(
'menu-container max-h-[calc(100vh-96px)] overflow-y-auto border-0',
className,
)}
style={style}
>
{children}
+55 -45
View File
@@ -4,6 +4,49 @@ import { useEffect, useRef, useState } from 'react';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
const getTriangleStyles = (
trianglePosition: Position | undefined,
size: number,
offset: number,
): React.CSSProperties => {
if (!trianglePosition) {
return { left: '-999px', top: '-999px' };
}
const { dir, point } = trianglePosition;
let topOffset = 0;
let leftOffset = 0;
switch (dir) {
case 'up':
topOffset = offset;
break;
case 'down':
topOffset = -offset;
break;
case 'left':
leftOffset = offset;
break;
case 'right':
leftOffset = -offset;
break;
}
return {
left: `${point.x + leftOffset}px`,
top: `${point.y + topOffset}px`,
borderLeft:
dir === 'right' ? 'none' : dir === 'left' ? `${size}px solid` : `${size}px solid transparent`,
borderRight:
dir === 'left' ? 'none' : dir === 'right' ? `${size}px solid` : `${size}px solid transparent`,
borderTop:
dir === 'down' ? 'none' : dir === 'up' ? `${size}px solid` : `${size}px solid transparent`,
borderBottom:
dir === 'up' ? 'none' : dir === 'down' ? `${size}px solid` : `${size}px solid transparent`,
transform: dir === 'left' || dir === 'right' ? 'translateY(-50%)' : 'translateX(-50%)',
};
};
const Popup = ({
width,
height,
@@ -82,14 +125,22 @@ const Popup = ({
setAdjustedPosition(newPosition);
}, [position, trianglePosition, popupPadding, childrenHeight]);
const outerTriangleStyles = getTriangleStyles(trianglePosition, 7, 0);
const innerTriangleStyles = getTriangleStyles(trianglePosition, 7, -1);
return (
<div>
<div
className={clsx('popup-triangle-outer text-base-300 absolute z-50', triangleClassName)}
style={outerTriangleStyles}
/>
<div
id='popup-container'
ref={containerRef}
className={clsx(
'bg-base-300 absolute z-50 rounded-lg font-sans',
trianglePosition?.dir !== 'up' && 'shadow-xl',
'popup-container bg-base-300 absolute z-50 rounded-lg font-sans',
'eink:border eink:border-base-content',
trianglePosition?.dir !== 'up' && 'not-eink:shadow-xl',
className,
)}
style={{
@@ -105,49 +156,8 @@ const Popup = ({
{children}
</div>
<div
className={`triangle text-base-300 absolute z-50 ${triangleClassName}`}
style={{
left:
trianglePosition?.dir === 'left'
? `${trianglePosition.point.x}px`
: trianglePosition?.dir === 'right'
? `${trianglePosition.point.x}px`
: `${trianglePosition ? trianglePosition.point.x : -999}px`,
top:
trianglePosition?.dir === 'up'
? `${trianglePosition.point.y}px`
: trianglePosition?.dir === 'down'
? `${trianglePosition.point.y}px`
: `${trianglePosition ? trianglePosition.point.y : -999}px`,
borderLeft:
trianglePosition?.dir === 'right'
? 'none'
: trianglePosition?.dir === 'left'
? `7px solid`
: '7px solid transparent',
borderRight:
trianglePosition?.dir === 'left'
? 'none'
: trianglePosition?.dir === 'right'
? `7px solid`
: '7px solid transparent',
borderTop:
trianglePosition?.dir === 'down'
? 'none'
: trianglePosition?.dir === 'up'
? `7px solid`
: '7px solid transparent',
borderBottom:
trianglePosition?.dir === 'up'
? 'none'
: trianglePosition?.dir === 'down'
? `7px solid`
: '7px solid transparent',
transform:
trianglePosition?.dir === 'left' || trianglePosition?.dir === 'right'
? 'translateY(-50%)'
: 'translateX(-50%)',
}}
className={clsx('popup-triangle-inner text-base-300 absolute z-50', triangleClassName)}
style={innerTriangleStyles}
/>
</div>
);
+4 -2
View File
@@ -19,8 +19,10 @@ const Spinner: React.FC<{
}}
role='status'
>
<span className={clsx('loading loading-dots loading-lg', className)}></span>
<span className='hidden'>{_('Loading...')}</span>
<span
className={clsx('loading loading-lg not-eink:loading-dots eink:loading-spinner', className)}
></span>
<span className='sr-only'>{_('Loading...')}</span>
</div>
);
};
+5 -3
View File
@@ -23,9 +23,9 @@ export const Toast = () => {
const alertClassMap = {
info: 'alert-primary border-base-300',
success: 'alert-success border-0 bg-gradient-to-r from-green-500 to-emerald-500',
warning: 'alert-warning border-0 bg-gradient-to-r from-amber-500 to-orange-500',
error: 'alert-error border-0 bg-gradient-to-r from-red-500 to-rose-500',
success: 'alert-success not-eink:from-green-500 not-eink:to-emerald-500',
warning: 'alert-warning not-eink:from-amber-500 not-eink:to-orange-500',
error: 'alert-error not-eink:from-red-500 not-eink:to-rose-500',
};
const iconMap = {
@@ -132,7 +132,9 @@ export const Toast = () => {
className={clsx(
'alert flex items-center gap-3 shadow-2xl backdrop-blur-sm',
'min-h-0 rounded-2xl px-5 py-4',
'not-eink:bg-gradient-to-r border-0',
alertClassMap[toastType],
'eink:bg-base-100 eink:border eink:border-base-content',
toastType !== 'info' && 'text-white',
)}
>
@@ -402,7 +402,7 @@ export const UpdaterContent = ({
</div>
<div className='text-base-content text-sm'>
<h3 className='mb-2 font-bold'>{_('Changelog')}</h3>
<div className='bg-base-200 mb-4 rounded-lg px-4 pb-2 pt-4'>
<div className='not-eink:bg-base-200 not-eink:px-4 mb-4 rounded-lg pb-2 pt-4'>
{changelogs.length > 0 ? (
changelogs.map((entry: Changelog) => (
<div key={entry.version} className='mb-4'>
@@ -186,7 +186,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
},
...annotationToolQuickActions.map((button) => ({
value: button.type,
label: button.label,
label: _(button.label),
})),
];
};
+14 -8
View File
@@ -1,14 +1,20 @@
import { useThemeStore } from '@/store/themeStore';
import { useCallback } from 'react';
export const useEinkMode = () => {
const applyEinkMode = useCallback((isEink: boolean) => {
if (isEink) {
document.body.classList.add('no-transitions');
} else {
document.body.classList.remove('no-transitions');
}
document.documentElement.setAttribute('data-eink', isEink.toString());
}, []);
const { setIsEinkMode } = useThemeStore();
const applyEinkMode = useCallback(
(isEink: boolean) => {
setIsEinkMode(isEink);
if (isEink) {
document.body.classList.add('no-transitions');
} else {
document.body.classList.remove('no-transitions');
}
document.documentElement.setAttribute('data-eink', isEink.toString());
},
[setIsEinkMode],
);
return { applyEinkMode };
};
+7 -2
View File
@@ -21,6 +21,7 @@ export const useTheme = ({
const {
themeColor,
isDarkMode,
isEinkMode,
showSystemUI,
dismissSystemUI,
updateAppTheme,
@@ -117,13 +118,17 @@ export const useTheme = ({
const colorScheme = isDarkMode ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', `${themeColor}-${colorScheme}`);
document.documentElement.style.setProperty('color-scheme', colorScheme);
document.documentElement.style.setProperty(
'--overlayer-highlight-opacity',
isEinkMode ? '1.0' : '0.3',
);
document.documentElement.style.setProperty(
'--overlayer-highlight-blend-mode',
isDarkMode ? 'lighten' : 'normal',
isEinkMode ? 'difference' : isDarkMode ? 'lighten' : 'normal',
);
document.documentElement.style.setProperty(
'--bg-texture-blend-mode',
isDarkMode ? 'lighten' : 'multiply',
);
}, [themeColor, isDarkMode]);
}, [themeColor, isDarkMode, isEinkMode]);
};
@@ -55,6 +55,7 @@ import {
SETTINGS_FILENAME,
DEFAULT_MOBILE_SYSTEM_SETTINGS,
DEFAULT_ANNOTATOR_CONFIG,
DEFAULT_EINK_VIEW_SETTINGS,
} from './constants';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { getOSPlatform, getTargetLang, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
@@ -88,6 +89,7 @@ export abstract class BaseAppService implements AppService {
isMobileApp = false;
isPortableApp = false;
isDesktopApp = false;
isEink = false;
hasTrafficLight = false;
hasWindow = false;
hasWindowBar = false;
@@ -200,6 +202,7 @@ export abstract class BaseAppService implements AppService {
...DEFAULT_BOOK_FONT,
...DEFAULT_BOOK_LANGUAGE,
...(this.isMobile ? DEFAULT_MOBILE_VIEW_SETTINGS : {}),
...(this.isEink ? DEFAULT_EINK_VIEW_SETTINGS : {}),
...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}),
...DEFAULT_VIEW_CONFIG,
...DEFAULT_TTS_CONFIG,
@@ -211,6 +211,12 @@ export const DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS: Partial<ViewSettings> = {
overrideColor: false,
};
export const DEFAULT_EINK_VIEW_SETTINGS: Partial<ViewSettings> = {
isEink: true,
animated: false,
volumeKeysToFlip: true,
};
export const DEFAULT_VIEW_CONFIG: ViewConfig = {
sideBarTab: 'toc',
uiLanguage: '',
@@ -52,6 +52,7 @@ import {
declare global {
interface Window {
__READEST_UPDATER_DISABLED?: boolean;
__READEST_IS_EINK?: boolean;
}
}
@@ -367,6 +368,7 @@ export class NativeAppService extends BaseAppService {
override isLinuxApp = OS_TYPE === 'linux';
override isMobileApp = ['android', 'ios'].includes(OS_TYPE);
override isDesktopApp = ['macos', 'windows', 'linux'].includes(OS_TYPE);
override isEink = Boolean(window.__READEST_IS_EINK);
override hasTrafficLight = OS_TYPE === 'macos';
override hasWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
override hasWindowBar = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
+14 -1
View File
@@ -8,12 +8,19 @@ import { EnvConfigType, isWebAppPlatform } from '@/services/environment';
import { SystemSettings } from '@/types/settings';
import { Insets } from '@/types/misc';
declare global {
interface Window {
__READEST_IS_EINK?: boolean;
}
}
interface ThemeState {
themeMode: ThemeMode;
themeColor: string;
systemIsDarkMode: boolean;
themeCode: ThemeCode;
isDarkMode: boolean;
isEinkMode: boolean;
systemUIVisible: boolean;
statusBarHeight: number;
systemUIAlwaysHidden: boolean;
@@ -26,6 +33,7 @@ interface ThemeState {
getIsDarkMode: () => boolean;
setThemeMode: (mode: ThemeMode) => void;
setThemeColor: (color: string) => void;
setIsEinkMode: (isEink: boolean) => void;
updateAppTheme: (color: keyof Palette) => void;
saveCustomTheme: (
envConfig: EnvConfigType,
@@ -46,7 +54,8 @@ const getInitialThemeMode = (): ThemeMode => {
const getInitialThemeColor = (): string => {
if (typeof window !== 'undefined' && localStorage) {
return localStorage.getItem('themeColor') || 'default';
const defaultColor = window.__READEST_IS_EINK ? 'contrast' : 'default';
return localStorage.getItem('themeColor') || defaultColor;
}
return 'default';
};
@@ -65,6 +74,7 @@ export const useThemeStore = create<ThemeState>((set, get) => {
themeColor: initialThemeColor,
systemIsDarkMode,
isDarkMode,
isEinkMode: false,
themeCode,
systemUIVisible: false,
statusBarHeight: 24,
@@ -76,6 +86,9 @@ export const useThemeStore = create<ThemeState>((set, get) => {
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
setSystemUIAlwaysHidden: (hidden: boolean) => set({ systemUIAlwaysHidden: hidden }),
getIsDarkMode: () => get().isDarkMode,
setIsEinkMode: (isEink: boolean) => {
set({ isEinkMode: isEink });
},
setThemeMode: (mode) => {
if (typeof window !== 'undefined' && localStorage) {
localStorage.setItem('themeMode', mode);
+109
View File
@@ -171,6 +171,15 @@ foliate-fxl {
border-bottom: 12px solid theme('colors.base-200');
z-index: 1;
}
.dropdown-content.bordercolor-content::before {
border-bottom: 12px solid theme('colors.base-content');
z-index: 1;
}
.dropdown-content.bordercolor-content::after {
top: -11px;
border-bottom: 13px solid theme('colors.base-100');
z-index: 1;
}
.dropdown-left::before,
.dropdown-left::after {
@@ -462,8 +471,14 @@ foliate-fxl {
color: theme('colors.base-content') !important;
}
[data-eink="true"] [class*="text-blue"] {
color: theme('colors.base-content') !important;
font-weight: normal;
}
[data-eink="true"] [class*="text-red"] {
color: theme('colors.base-content') !important;
font-weight: normal;
}
[data-eink="true"] [class*="text-neutral-content"] {
@@ -486,6 +501,100 @@ foliate-fxl {
font-weight: bold;
}
[data-eink="true"] .eink-bordered {
border-width: 1px !important;
border-color: theme('colors.base-content') !important;
background-color: theme('colors.base-100') !important;
}
[data-eink="true"] input.search-input, [data-eink="true"] input.search-input:focus {
border-width: 1px !important;
border-radius: 9999px;
border-color: theme('colors.base-content') !important;
background-color: theme('colors.base-100') !important;
}
[data-eink="true"] .modal-box {
box-shadow: none !important;
border-width: 1px !important;
border-color: theme('colors.base-content') !important;
}
[data-eink="true"] .dialog-overlay {
background-color: transparent !important;
}
[data-eink="true"] .menu-container {
background-color: theme('colors.base-100') !important;
border: 1px solid theme('colors.base-content') !important;
}
[data-eink="true"] hr {
border-color: theme('colors.base-content/0.75') !important;
}
[data-eink="true"] button, [data-eink="true"] .btn {
color: theme('colors.base-content') !important;
}
[data-eink="true"] .btn-primary {
background-color: theme('colors.base-100') !important;
border: 1px solid theme('colors.base-content') !important;
color: theme('colors.base-content') !important;
}
[data-eink="true"] .bg-primary {
background-color: theme('colors.base-100') !important;
border: 1px solid theme('colors.base-content') !important;
color: theme('colors.base-content') !important;
}
[data-eink="true"] .dropdown-toggle {
background-color: theme('colors.transparent') !important;
}
[data-eink="true"] .opds-navigation .card {
background-color: theme('colors.base-100') !important;
border: 1px solid theme('colors.base-content') !important;
}
[data-eink="true"] .booknote-item {
background-color: theme('colors.base-100') !important;
border: 1px solid theme('colors.base-content') !important;
}
[data-eink="true"] .booknote-text {
background-color: theme('colors.base-100') !important;
}
[data-eink="true"] .bookitem-main, [data-eink="true"] .groupitem-main {
border: 1px solid theme('colors.base-content') !important;
}
[data-eink="true"] .popup-container {
border: 1px solid theme('colors.base-content') !important;
background-color: theme('colors.base-100') !important;
}
[data-eink="true"] .popup-triangle-outer {
color: theme('colors.base-content') !important;
}
[data-eink="true"] .popup-triangle-inner {
color: theme('colors.base-100') !important;
}
[data-eink="true"] .annotation-notes {
transform-origin: top left;
transform: translate(-1px, -1px);
}
[data-eink="true"] .alert {
color: theme('colors.base-content') !important;
border: 1px solid theme('colors.base-content') !important;
background-color: theme('colors.base-100') !important;
}
.no-transitions * {
transition: none !important;
}
+1
View File
@@ -85,6 +85,7 @@ export interface AppService {
isLinuxApp: boolean;
isPortableApp: boolean;
isDesktopApp: boolean;
isEink: boolean;
canCustomizeRootDir: boolean;
canReadExternalDir: boolean;
distChannel: DistChannel;
+9 -1
View File
@@ -2,6 +2,7 @@ import type { Config } from 'tailwindcss';
import { themes } from './src/styles/themes';
import daisyui from 'daisyui';
import typography from '@tailwindcss/typography';
import plugin from 'tailwindcss/plugin';
const config: Config = {
content: [
@@ -24,7 +25,14 @@ const config: Config = {
},
},
},
plugins: [daisyui, typography],
plugins: [
daisyui,
typography,
plugin(function ({ addVariant }) {
addVariant('eink', 'html[data-eink="true"] &');
addVariant('not-eink', 'html:not([data-eink="true"]) &');
}),
],
daisyui: {
themes: themes.reduce(
(acc, { name, colors }) => {