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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
export interface ThrottleOptions {
|
||||
interface ThrottleOptions {
|
||||
emitLast?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user