layout: overlay the footer of the translator popup and less sensitive page flip with mouse wheel (#1238)

* layout: overlay the footer of the translator popup

* fix: less sensitive page flip with mouse wheel, addresses #1165
This commit is contained in:
Huang Xin
2025-05-25 15:32:00 +08:00
committed by GitHub
parent e6428656c2
commit 033e161455
4 changed files with 64 additions and 29 deletions
@@ -170,7 +170,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
<div className='mx-4 flex-shrink-0 border-t border-gray-500/30'></div>
<div className='overflow-y-auto px-4 pt-4 font-sans'>
<div className='overflow-y-auto px-4 pb-8 pt-4 font-sans'>
<div className='mb-2 flex items-center justify-between'>
<h2 className='text-sm font-normal'>{_('Translated Text')}</h2>
<Select
@@ -181,7 +181,6 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
.map(([code, name]) => ({ value: code, label: name }))}
/>
</div>
{loading ? (
<p className='text-base italic text-gray-500'>{_('Loading...')}</p>
) : (
@@ -193,25 +192,27 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
{translation || _('No translation available.')}
</p>
)}
<div className='flex h-10 items-center justify-between pt-4'>
{provider && (
<div className='text-xs opacity-60'>
{error
? ''
: _('Translated by {{provider}}.', {
provider: providers.find((p) => p.name === provider)?.label,
})}
</div>
)}
<Select
value={provider}
onChange={handleProviderChange}
options={providers.map(({ name: value, label }) => ({ value, label }))}
/>
</div>
</div>
)}
</div>
<div className='absolute bottom-0 flex h-8 w-full items-center justify-between bg-gray-600 px-4'>
{provider && !loading && (
<div className='text-xs opacity-60'>
{error
? ''
: _('Translated by {{provider}}.', {
provider: providers.find((p) => p.name === provider)?.label,
})}
</div>
)}
<div className='ml-auto'>
<Select
value={provider}
onChange={handleProviderChange}
options={providers.map(({ name: value, label }) => ({ value, label }))}
/>
</div>
</div>
</Popup>
</div>
);
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useReaderStore } from '@/store/readerStore';
import { throttle } from '@/utils/throttle';
import { debounce } from '@/utils/debounce';
import { ScrollSource } from './usePagination';
export const useMouseEvent = (
@@ -9,27 +9,23 @@ export const useMouseEvent = (
handleContinuousScroll: (source: ScrollSource, delta: number, threshold: number) => void,
) => {
const { hoveredBookKey } = useReaderStore();
const throttledScroll = throttle(handleContinuousScroll, 500, {
emitLast: false,
});
const throttledFlip = throttle(handlePageFlip, 1000, {
emitLast: false,
});
const debounceScroll = debounce(handleContinuousScroll, 500);
const debounceFlip = debounce(handlePageFlip, 100);
const handleMouseEvent = (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (msg instanceof MessageEvent) {
if (msg.data && msg.data.bookKey === bookKey) {
if (msg.data.type === 'iframe-wheel') {
throttledScroll('mouse', -msg.data.deltaY, 0);
debounceScroll('mouse', -msg.data.deltaY, 0);
}
if (msg.data.type === 'iframe-wheel') {
throttledFlip(msg);
debounceFlip(msg);
} else {
handlePageFlip(msg);
}
}
} else if (msg.type === 'wheel') {
const event = msg as React.WheelEvent<HTMLDivElement>;
throttledScroll('mouse', -event.deltaY, 0);
debounceScroll('mouse', -event.deltaY, 0);
} else {
handlePageFlip(msg);
}
+38
View File
@@ -0,0 +1,38 @@
interface DebounceOptions {
emitLast?: boolean;
}
/**
* Debounces a function by waiting `delay` ms after the last call before executing it.
* If `emitLast` is false, it cancels the call instead of delaying it.
*/
export const debounce = <T extends (...args: Parameters<T>) => void | Promise<void>>(
func: T,
delay: number,
options: DebounceOptions = { emitLast: true },
): ((...args: Parameters<T>) => void) => {
let timeout: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
return (...args: Parameters<T>): void => {
if (timeout) {
clearTimeout(timeout);
}
if (options.emitLast) {
lastArgs = args;
timeout = setTimeout(() => {
if (lastArgs) {
func(...(lastArgs as Parameters<T>));
lastArgs = null;
}
timeout = null;
}, delay);
} else {
timeout = setTimeout(() => {
func(...args);
timeout = null;
}, delay);
}
};
};
+1 -1
View File
@@ -1,4 +1,4 @@
export interface ThrottleOptions {
interface ThrottleOptions {
emitLast?: boolean;
}