forked from akai/readest
feat(eink): optimize color and layout for e-ink mode (#2887)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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')}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -85,6 +85,7 @@ export interface AppService {
|
||||
isLinuxApp: boolean;
|
||||
isPortableApp: boolean;
|
||||
isDesktopApp: boolean;
|
||||
isEink: boolean;
|
||||
canCustomizeRootDir: boolean;
|
||||
canReadExternalDir: boolean;
|
||||
distChannel: DistChannel;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user