This commit is contained in:
@@ -183,7 +183,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'footer-bar shadow-xs bottom-0 z-50 flex w-full flex-col',
|
||||
'footer-bar shadow-xs bottom-0 z-30 flex w-full flex-col',
|
||||
'sm:h-[52px] sm:justify-center',
|
||||
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
|
||||
'transition-[opacity,transform] duration-300',
|
||||
@@ -211,7 +211,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<div className='flex w-full items-center justify-between gap-x-6'>
|
||||
@@ -265,7 +265,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
|
||||
)}
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom + 64}px` : '64px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33 + 64}px` : '64px',
|
||||
}}
|
||||
>
|
||||
<Slider
|
||||
@@ -304,10 +304,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
'bg-base-200 z-30 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
|
||||
)}
|
||||
style={{
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom + 16}px` : '0px',
|
||||
paddingBottom: isMobile ? `${gridInsets.bottom * 0.33 + 16}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -337,7 +337,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{
|
||||
bottom: isMobile ? `${gridInsets.bottom / 2}px` : '0px',
|
||||
bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -392,7 +392,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TTSControl bookKey={bookKey} />
|
||||
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
className={clsx(
|
||||
'progressinfo absolute bottom-0 flex items-center justify-between',
|
||||
'text-neutral-content font-sans text-xs font-extralight',
|
||||
isVertical ? 'writing-vertical-rl' : 'h-[52px] w-full',
|
||||
isVertical ? 'writing-vertical-rl' : 'w-full',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
style={
|
||||
@@ -78,16 +78,23 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
: {
|
||||
paddingInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
|
||||
paddingInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.67}px` : 0,
|
||||
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className='text-start'>{timeLeft}</span>
|
||||
) : viewSettings.showRemainingPages ? (
|
||||
<span className='text-start'>{pageLeft}</span>
|
||||
) : null}
|
||||
{viewSettings.showProgressInfo && <span className='ms-auto text-end'>{progressInfo}</span>}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center',
|
||||
isVertical ? 'h-full' : 'h-[52px] w-full',
|
||||
)}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className='text-start'>{timeLeft}</span>
|
||||
) : viewSettings.showRemainingPages ? (
|
||||
<span className='text-start'>{pageLeft}</span>
|
||||
) : null}
|
||||
{viewSettings.showProgressInfo && <span className='ms-auto text-end'>{progressInfo}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,27 @@ import { getLocale } from '@/utils/misc';
|
||||
import { initDayjs } from '@/utils/time';
|
||||
import ReaderContent from './ReaderContent';
|
||||
|
||||
/*
|
||||
Z-Index Layering Guide:
|
||||
---------------------------------
|
||||
99 – Window Border (Linux only)
|
||||
• Ensures the border stays on top of all UI elements.
|
||||
50 – Loading Progress / Toast Notifications / Dialogs
|
||||
• Includes Settings, About, Updater, and KOSync dialogs.
|
||||
45 – Sidebar / Notebook (Unpinned)
|
||||
• Floats above the content but below global dialogs.
|
||||
40 – TTS Bar
|
||||
• Mini controls for TTS playback on top of the TTS Control.
|
||||
30 – Footbar (TTS Control)
|
||||
• Persistent bottom controls and the TTS icon/panel.
|
||||
20 – Menu / Sidebar / Notebook (Pinned)
|
||||
• Docked navigation or note views.
|
||||
10 – Headerbar / Ribbon
|
||||
• Top toolbar and ribbon elements.
|
||||
0 – Base Content
|
||||
• Main reading area or background content.
|
||||
*/
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const router = useRouter();
|
||||
const { envConfig, appService } = useEnv();
|
||||
|
||||
@@ -67,10 +67,12 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
if (index === 0) setSideBarBookKey(key);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowBookDetails = (event: CustomEvent) => {
|
||||
const book = event.detail as Book;
|
||||
setShowDetailsBook(book);
|
||||
setShowDetailsBook(event.detail as Book);
|
||||
return true;
|
||||
};
|
||||
eventDispatcher.onSync('show-book-details', handleShowBookDetails);
|
||||
@@ -78,7 +80,6 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
return () => {
|
||||
eventDispatcher.offSync('show-book-details', handleShowBookDetails);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -189,15 +189,15 @@ const Notebook: React.FC = ({}) => {
|
||||
return isNotebookVisible ? (
|
||||
<>
|
||||
{!isNotebookPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
<div className='overlay fixed inset-0 z-[45] bg-black/20' onClick={handleClickOverlay} />
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 flex min-w-60 select-none flex-col',
|
||||
'notebook-container bg-base-200 right-0 flex min-w-60 select-none flex-col',
|
||||
'font-sans text-base font-normal sm:text-sm',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isNotebookPinned && 'shadow-2xl',
|
||||
isNotebookPinned ? 'z-20' : 'z-[45] shadow-2xl',
|
||||
)}
|
||||
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
|
||||
style={{
|
||||
|
||||
@@ -3,6 +3,8 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
@@ -16,11 +18,14 @@ const SidebarContent: React.FC<{
|
||||
sideBarBookKey: string;
|
||||
}> = ({ bookDoc, sideBarBookKey }) => {
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const { setSideBarVisible } = useSidebarStore();
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const [activeTab, setActiveTab] = useState(config?.viewSettings?.sideBarTab || 'toc');
|
||||
const [fade, setFade] = useState(false);
|
||||
const [targetTab, setTargetTab] = useState(activeTab);
|
||||
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
|
||||
|
||||
useEffect(() => {
|
||||
if (!sideBarBookKey) return;
|
||||
@@ -32,8 +37,13 @@ const SidebarContent: React.FC<{
|
||||
const handleTabChange = (tab: string) => {
|
||||
setFade(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setFade(false);
|
||||
if (activeTab === tab && isMobile) {
|
||||
setHoveredBookKey(sideBarBookKey);
|
||||
setSideBarVisible(false);
|
||||
return;
|
||||
}
|
||||
setTargetTab(tab);
|
||||
setFade(false);
|
||||
setConfig(sideBarBookKey!, config);
|
||||
clearTimeout(timeout);
|
||||
}, 300);
|
||||
|
||||
@@ -181,13 +181,19 @@ const SideBar: React.FC<{
|
||||
|
||||
return isSideBarVisible ? (
|
||||
<>
|
||||
{!isSideBarPinned && (
|
||||
<div
|
||||
className='overlay fixed inset-0 z-[45] bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 flex min-w-60 select-none flex-col',
|
||||
'sidebar-container bg-base-200 flex min-w-60 select-none flex-col',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
'transition-[padding-top] duration-300',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isSideBarPinned && 'shadow-2xl',
|
||||
isSideBarPinned ? 'z-20' : 'z-[45] shadow-2xl',
|
||||
)}
|
||||
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
|
||||
style={{
|
||||
@@ -266,12 +272,6 @@ const SideBar: React.FC<{
|
||||
onTouchStart={handleHorizontalDragStart}
|
||||
></div>
|
||||
</div>
|
||||
{!isSideBarPinned && (
|
||||
<div
|
||||
className='overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const TabNavigation: React.FC<{
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-300 absolute bottom-1.5 start-1 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
|
||||
'bg-base-300 absolute bottom-1.5 start-1 z-10 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
|
||||
'transform transition-transform duration-300',
|
||||
activeTab === 'toc' && 'translate-x-0',
|
||||
activeTab === 'annotations' && 'translate-x-[calc(100%+8px)]',
|
||||
@@ -36,7 +36,7 @@ const TabNavigation: React.FC<{
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab}
|
||||
className='lg:tooltip lg:tooltip-top z-50 m-1.5 flex-1 cursor-pointer rounded-md p-2'
|
||||
className='lg:tooltip lg:tooltip-top z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
|
||||
data-tip={
|
||||
tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { MdPlayArrow, MdOutlinePause, MdFastRewind, MdFastForward } from 'react-icons/md';
|
||||
import { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
type TTSBarProps = {
|
||||
bookKey: string;
|
||||
isPlaying: boolean;
|
||||
onTogglePlay: () => void;
|
||||
onBackward: () => void;
|
||||
onForward: () => void;
|
||||
gridInsets: Insets;
|
||||
};
|
||||
|
||||
const TTSBar = ({
|
||||
bookKey,
|
||||
isPlaying,
|
||||
onTogglePlay,
|
||||
onBackward,
|
||||
onForward,
|
||||
gridInsets,
|
||||
}: TTSBarProps) => {
|
||||
const { appService } = useEnv();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const iconSize32 = useResponsiveSize(30);
|
||||
const iconSize48 = useResponsiveSize(36);
|
||||
|
||||
const isVisible = hoveredBookKey !== bookKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 absolute bottom-0 z-40',
|
||||
'inset-x-0 mx-auto flex w-full justify-center sm:w-fit',
|
||||
'transition-opacity duration-300',
|
||||
isVisible ? `pointer-events-auto opacity-100` : `pointer-events-none opacity-0`,
|
||||
)}
|
||||
style={{ paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0 }}
|
||||
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
onTouchStart={() => !appService?.isMobile && setHoveredBookKey('')}
|
||||
>
|
||||
<div className='text-base-content flex h-[52px] items-center space-x-2 px-2'>
|
||||
<button
|
||||
onClick={onBackward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
{isPlaying ? <MdOutlinePause size={iconSize48} /> : <MdPlayArrow size={iconSize48} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onForward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSBar;
|
||||
@@ -13,30 +13,35 @@ import { parseSSMLLang } from '@/utils/ssml';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { invokeUseBackgroundAudio } from '@/utils/bridge';
|
||||
import { CFI } from '@/libs/document';
|
||||
import { Insets } from '@/types/misc';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
import TTSBar from './TTSBar';
|
||||
|
||||
const POPUP_WIDTH = 282;
|
||||
const POPUP_HEIGHT = 160;
|
||||
const POPUP_HEIGHT = 180;
|
||||
const POPUP_PADDING = 10;
|
||||
|
||||
interface TTSControlProps {
|
||||
bookKey: string;
|
||||
gridInsets: Insets;
|
||||
}
|
||||
|
||||
const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getProgress, getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, getView, getProgress, getViewSettings } = useReaderStore();
|
||||
const { setViewSettings, setTTSEnabled } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const [ttsLang, setTtsLang] = useState<string>('en');
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showIndicator, setShowIndicator] = useState(false);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [showTTSBar, setShowTTSBar] = useState(() => !!viewSettings?.showTTSBar);
|
||||
const [panelPosition, setPanelPosition] = useState<Position>();
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
|
||||
@@ -44,7 +49,6 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
|
||||
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const popupPadding = useResponsiveSize(POPUP_PADDING);
|
||||
const maxWidth = window.innerWidth - 2 * popupPadding;
|
||||
const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH));
|
||||
@@ -363,6 +367,16 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTTSBar = () => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewSettings.showTTSBar = !viewSettings.showTTSBar;
|
||||
setShowTTSBar(viewSettings.showTTSBar);
|
||||
if (viewSettings.showTTSBar) {
|
||||
setShowPanel(false);
|
||||
}
|
||||
setViewSettings(bookKey, viewSettings);
|
||||
};
|
||||
|
||||
const updatePanelPosition = () => {
|
||||
if (iconRef.current) {
|
||||
const rect = iconRef.current.getBoundingClientRect();
|
||||
@@ -430,12 +444,13 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
ref={iconRef}
|
||||
className={clsx(
|
||||
'absolute h-12 w-12',
|
||||
viewSettings?.rtl ? 'left-6' : 'right-6',
|
||||
'transition-transform duration-300',
|
||||
viewSettings?.rtl ? 'left-8' : 'right-6',
|
||||
!appService?.hasSafeAreaInset && 'bottom-[70px] sm:bottom-14',
|
||||
)}
|
||||
style={{
|
||||
bottom: appService?.hasSafeAreaInset
|
||||
? `${(safeAreaInsets?.bottom || 0) + 70}px`
|
||||
? `${(safeAreaInsets?.bottom || 0) * 0.33 + (hoveredBookKey ? 70 : 52)}px`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
@@ -464,9 +479,20 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey }) => {
|
||||
onSetVoice={handleSetVoice}
|
||||
onGetVoiceId={handleGetVoiceId}
|
||||
onSelectTimeout={handleSelectTimeout}
|
||||
onToogleTTSBar={handleToggleTTSBar}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
{showIndicator && showTTSBar && ttsClientsInited && (
|
||||
<TTSBar
|
||||
bookKey={bookKey}
|
||||
isPlaying={isPlaying}
|
||||
onBackward={handleBackward}
|
||||
onTogglePlay={handleTogglePlay}
|
||||
onForward={handleForward}
|
||||
gridInsets={gridInsets}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdAlarm } from 'react-icons/md';
|
||||
import { TbChevronCompactDown, TbChevronCompactUp } from 'react-icons/tb';
|
||||
import { RiVoiceAiFill } from 'react-icons/ri';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { TTSVoicesGroup } from '@/services/tts';
|
||||
@@ -25,6 +26,7 @@ type TTSPanelProps = {
|
||||
onSetVoice: (voice: string, lang: string) => void;
|
||||
onGetVoiceId: () => string;
|
||||
onSelectTimeout: (bookKey: string, value: number) => void;
|
||||
onToogleTTSBar: () => void;
|
||||
};
|
||||
|
||||
const getTTSTimeoutOptions = (_: TranslationFunc) => {
|
||||
@@ -113,6 +115,7 @@ const TTSPanel = ({
|
||||
onSetVoice,
|
||||
onGetVoiceId,
|
||||
onSelectTimeout,
|
||||
onToogleTTSBar,
|
||||
}: TTSPanelProps) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
@@ -200,7 +203,7 @@ const TTSPanel = ({
|
||||
const timeoutOptions = getTTSTimeoutOptions(_);
|
||||
|
||||
return (
|
||||
<div className='flex w-full flex-col items-center justify-center gap-2 rounded-2xl p-4'>
|
||||
<div className='flex w-full flex-col items-center justify-center gap-2 rounded-2xl px-4 pt-4'>
|
||||
<div className='flex w-full flex-col items-center gap-0.5'>
|
||||
<input
|
||||
className='range'
|
||||
@@ -231,23 +234,32 @@ const TTSPanel = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<button onClick={onBackward} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onBackward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onTogglePlay} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
{isPlaying ? (
|
||||
<MdPauseCircle size={iconSize48} className='fill-primary' />
|
||||
) : (
|
||||
<MdPlayCircle size={iconSize48} className='fill-primary' />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onForward} className='rounded-full p-1'>
|
||||
<button
|
||||
onClick={onForward}
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
>
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='flex flex-col items-center justify-center rounded-full p-1'
|
||||
className='flex flex-col items-center justify-center rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<MdAlarm size={iconSize32} />
|
||||
@@ -293,7 +305,7 @@ const TTSPanel = ({
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className='rounded-full p-1'
|
||||
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
>
|
||||
<RiVoiceAiFill size={iconSize32} />
|
||||
@@ -350,6 +362,15 @@ const TTSPanel = ({
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-6 items-center justify-center opacity-60 transition-transform duration-200 hover:scale-105 hover:opacity-100'>
|
||||
<button onClick={onToogleTTSBar} className='p-0'>
|
||||
{viewSettings?.showTTSBar ? (
|
||||
<TbChevronCompactUp size={iconSize48} style={{ transform: 'scaleY(0.85)' }} />
|
||||
) : (
|
||||
<TbChevronCompactDown size={iconSize48} style={{ transform: 'scaleY(0.85)' }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -187,6 +187,7 @@ export const DEFAULT_TTS_CONFIG: TTSConfig = {
|
||||
ttsRate: 1.3,
|
||||
ttsVoice: '',
|
||||
ttsLocation: '',
|
||||
showTTSBar: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface TTSConfig {
|
||||
ttsRate: number;
|
||||
ttsVoice: string;
|
||||
ttsLocation: string;
|
||||
showTTSBar: boolean;
|
||||
}
|
||||
|
||||
export interface TranslatorConfig {
|
||||
|
||||
Reference in New Issue
Block a user