This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-responsive": "^10.0.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Ic
|
||||
'bg-base-100 border-gray-300 shadow-sm transition hover:bg-gray-50',
|
||||
)}
|
||||
>
|
||||
<Icon size={20} />
|
||||
<Icon />
|
||||
<span className='text-neutral-content/70 px-2 text-sm'>{label}</span>
|
||||
</button>
|
||||
);
|
||||
@@ -194,7 +194,7 @@ export default function AuthPage() {
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-3 top-11 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} className='text-base-content' />
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
|
||||
<ProviderLogin
|
||||
@@ -238,7 +238,7 @@ export default function AuthPage() {
|
||||
onClick={handleGoBack}
|
||||
className='btn btn-ghost fixed left-10 top-6 h-8 min-h-8 w-8 p-0'
|
||||
>
|
||||
<IoArrowBack size={20} className='text-base-content' />
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { navigateToReader } from '@/utils/nav';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getFilename } from '@/utils/book';
|
||||
@@ -73,6 +74,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
const isImportingBook = useRef(false);
|
||||
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
|
||||
const showBookDetailsModal = (book: Book) => {
|
||||
setShowDetailsBook(book);
|
||||
@@ -232,12 +234,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
{isSelectMode && (
|
||||
<div className='absolute bottom-1 right-1'>
|
||||
{selectedBooks.includes(item.hash) ? (
|
||||
<MdCheckCircle size={20} className='fill-blue-500' />
|
||||
<MdCheckCircle className='fill-blue-500' />
|
||||
) : (
|
||||
<MdCheckCircleOutline
|
||||
size={20}
|
||||
className='fill-gray-300 drop-shadow-sm'
|
||||
/>
|
||||
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +267,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CiCircleMore size={15} />
|
||||
<CiCircleMore size={iconSize15} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -285,7 +284,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CiCircleMore size={15} />
|
||||
<CiCircleMore size={iconSize15} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -101,7 +101,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={16} />}
|
||||
toggleButton={<MdOutlineMenu />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
|
||||
@@ -75,7 +75,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
height={20}
|
||||
/>
|
||||
) : (
|
||||
<PiUserCircleCheck size={20} />
|
||||
<PiUserCircleCheck />
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -84,11 +84,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
</ul>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
label={_('Sign In')}
|
||||
icon={<PiUserCircle size={20} />}
|
||||
onClick={handleUserLogin}
|
||||
></MenuItem>
|
||||
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
|
||||
)}
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
|
||||
@@ -97,9 +97,9 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
isBookmarked ? (
|
||||
<MdOutlineBookmark size={20} className='text-base-content' />
|
||||
<MdOutlineBookmark className='text-base-content' />
|
||||
) : (
|
||||
<MdOutlineBookmarkAdd size={20} className='text-base-content' />
|
||||
<MdOutlineBookmarkAdd className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={toggleBookmark}
|
||||
|
||||
@@ -70,19 +70,15 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
onMouseEnter={() => setHoveredBookKey(bookKey)}
|
||||
onMouseLeave={() => setHoveredBookKey('')}
|
||||
>
|
||||
<Button icon={<RiArrowLeftWideLine />} onClick={handleGoPrev} tooltip={_('Go Left')} />
|
||||
<Button
|
||||
icon={<RiArrowLeftWideLine size={20} />}
|
||||
onClick={handleGoPrev}
|
||||
tooltip={_('Go Left')}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoBackLine size={20} />}
|
||||
icon={<RiArrowGoBackLine />}
|
||||
onClick={handleGoBack}
|
||||
tooltip={_('Go Back')}
|
||||
disabled={!view?.history.canGoBack}
|
||||
/>
|
||||
<Button
|
||||
icon={<RiArrowGoForwardLine size={20} />}
|
||||
icon={<RiArrowGoForwardLine />}
|
||||
onClick={handleGoForward}
|
||||
tooltip={_('Go Forward')}
|
||||
disabled={!view?.history.canGoForward}
|
||||
@@ -98,12 +94,8 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
value={pageinfoValid ? progressFraction * 100 : 0}
|
||||
onChange={(e) => handleProgressChange(e)}
|
||||
/>
|
||||
<Button icon={<FaHeadphones size={20} />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<Button
|
||||
icon={<RiArrowRightWideLine size={20} />}
|
||||
onClick={handleGoNext}
|
||||
tooltip={_('Go Right')}
|
||||
/>
|
||||
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
|
||||
<Button icon={<RiArrowRightWideLine />} onClick={handleGoNext} tooltip={_('Go Right')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
toggleButton={<PiDotsThreeVerticalBold />}
|
||||
onToggle={handleToggleDropdown}
|
||||
>
|
||||
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
|
||||
|
||||
@@ -26,9 +26,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
sideBarBookKey == bookKey && isNotebookVisible ? (
|
||||
<LuNotebookPen size={16} className='text-base-content' />
|
||||
<LuNotebookPen className='text-base-content' />
|
||||
) : (
|
||||
<LuNotebookPen size={16} className='text-base-content' />
|
||||
<LuNotebookPen className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
|
||||
@@ -13,7 +13,7 @@ const SettingsToggler = () => {
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
icon={<RiFontSize size={16} className='text-base-content' />}
|
||||
icon={<RiFontSize className='text-base-content' />}
|
||||
onClick={handleToggleSettings}
|
||||
tooltip={_('Font & Layout')}
|
||||
tooltipDirection='bottom'
|
||||
|
||||
@@ -24,9 +24,9 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
<Button
|
||||
icon={
|
||||
sideBarBookKey === bookKey && isSideBarVisible ? (
|
||||
<TbLayoutSidebarFilled size={20} className='text-base-content' />
|
||||
<TbLayoutSidebarFilled className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebar size={20} className='text-base-content' />
|
||||
<TbLayoutSidebar className='text-base-content' />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleSidebar}
|
||||
|
||||
@@ -96,7 +96,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
zoomLevel <= MIN_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomOut size={20} />
|
||||
<MdZoomOut />
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
@@ -113,7 +113,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
zoomLevel >= MAX_ZOOM_LEVEL && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomIn size={20} />
|
||||
<MdZoomIn />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<MenuItem
|
||||
label={_('Scrolled Mode')}
|
||||
shortcut='Shift+J'
|
||||
icon={isScrolledMode ? <MdCheck size={20} /> : undefined}
|
||||
icon={isScrolledMode ? <MdCheck /> : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
/>
|
||||
|
||||
@@ -138,20 +138,12 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode')
|
||||
}
|
||||
icon={
|
||||
themeMode === 'dark' ? (
|
||||
<BiMoon size={20} />
|
||||
) : themeMode === 'light' ? (
|
||||
<BiSun size={20} />
|
||||
) : (
|
||||
<TbSunMoon size={20} />
|
||||
)
|
||||
}
|
||||
icon={themeMode === 'dark' ? <BiMoon /> : themeMode === 'light' ? <BiSun /> : <TbSunMoon />}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<MenuItem
|
||||
label={_('Invert Colors in Dark Mode')}
|
||||
icon={isInvertedColors ? <MdCheck size={20} className='text-base-content' /> : undefined}
|
||||
icon={isInvertedColors ? <MdCheck className='text-base-content' /> : undefined}
|
||||
onClick={toggleInvertedColors}
|
||||
disabled={!isDarkMode}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { FaCheckCircle } from 'react-icons/fa';
|
||||
import { HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const styles = ['highlight', 'underline', 'squiggly'] as HighlightStyle[];
|
||||
const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
|
||||
@@ -25,6 +25,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
const globalReadSettings = settings.globalReadSettings;
|
||||
const [selectedStyle, setSelectedStyle] = React.useState<HighlightStyle>(_selectedStyle);
|
||||
const [selectedColor, setSelectedColor] = React.useState<HighlightColor>(_selectedColor);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
const handleSelectStyle = (style: HighlightStyle) => {
|
||||
globalReadSettings.highlightStyle = style;
|
||||
@@ -84,7 +85,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
)}
|
||||
>
|
||||
{selectedColor === color && (
|
||||
<FaCheckCircle size={16} className={clsx(`fill-${color}-400`)} />
|
||||
<FaCheckCircle size={iconSize16} className={clsx(`fill-${color}-400`)} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -22,7 +22,7 @@ const PopupButton: React.FC<PopupButtonProps> = ({ showTooltip, tooltipText, Ico
|
||||
onClick={handleClick}
|
||||
className='my-2 flex h-8 min-h-8 w-8 items-center justify-center p-0'
|
||||
>
|
||||
<Icon size={20} />
|
||||
<Icon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ const NotebookHeader: React.FC<{
|
||||
return (
|
||||
<div className='notebook-header relative flex h-11 items-center px-3'>
|
||||
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
|
||||
<LuNotebookPen size={14} />
|
||||
<LuNotebookPen />
|
||||
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center gap-x-4'>
|
||||
@@ -26,16 +26,16 @@ const NotebookHeader: React.FC<{
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
{isPinned ? <MdPushPin /> : <MdOutlinePushPin />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
<FiSearch />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,10 @@ import { TbSunMoon } from 'react-icons/tb';
|
||||
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { themes } from '@/styles/themes';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
@@ -15,6 +16,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
useTheme();
|
||||
const { getViews, getViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const iconSize24 = useResponsiveSize(24);
|
||||
|
||||
useEffect(() => {
|
||||
getViews().forEach((view) => {
|
||||
@@ -33,7 +35,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('auto')}
|
||||
>
|
||||
<TbSunMoon size={20} />
|
||||
<TbSunMoon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +44,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('light')}
|
||||
>
|
||||
<MdOutlineLightMode size={20} />
|
||||
<MdOutlineLightMode />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +53,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('dark')}
|
||||
>
|
||||
<MdOutlineDarkMode size={20} />
|
||||
<MdOutlineDarkMode />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,9 +82,9 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className='hidden'
|
||||
/>
|
||||
{themeColor === name ? (
|
||||
<MdRadioButtonChecked size={24} />
|
||||
<MdRadioButtonChecked size={iconSize24} />
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={24} />
|
||||
<MdRadioButtonUnchecked size={iconSize24} />
|
||||
)}
|
||||
<span>{_(label)}</span>
|
||||
</label>
|
||||
|
||||
@@ -27,7 +27,7 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isFontLayoutSettingsGlobal && <MdCheck size={20} className='text-base-content' />}
|
||||
{isFontLayoutSettingsGlobal && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<div
|
||||
className='lg:tooltip'
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { FiChevronUp, FiChevronLeft } from 'react-icons/fi';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface DropdownProps {
|
||||
family?: string;
|
||||
@@ -22,6 +23,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
onGetFontFamily,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
return (
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button
|
||||
@@ -29,7 +31,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
className='btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case'
|
||||
>
|
||||
<span style={{ fontFamily: onGetFontFamily(selected, family ?? '') }}>{selected}</span>
|
||||
<FiChevronUp className='h-4 w-4' />
|
||||
<FiChevronUp size={iconSize16} />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
@@ -39,7 +41,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selected === option && <MdCheck size={20} className='text-base-content' />}
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>{option}</span>
|
||||
</div>
|
||||
@@ -49,7 +51,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
<li className='dropdown dropdown-left dropdown-top'>
|
||||
<div className='flex items-center px-0'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
<FiChevronLeft className='h-4 w-4' />
|
||||
<FiChevronLeft size={iconSize16} />
|
||||
</span>
|
||||
<span>{_('System Fonts')}</span>
|
||||
</div>
|
||||
@@ -64,7 +66,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
<li key={option} onClick={() => onSelect(option)}>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selected === option && <MdCheck size={20} className='text-base-content' />}
|
||||
{selected === option && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
|
||||
{option}
|
||||
|
||||
@@ -153,7 +153,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('auto')}
|
||||
>
|
||||
<MdOutlineAutoMode size={20} />
|
||||
<MdOutlineAutoMode />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('horizontal-tb')}
|
||||
>
|
||||
<MdOutlineTextRotationNone size={20} />
|
||||
<MdOutlineTextRotationNone />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
className={`btn btn-ghost btn-circle ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => setWritingMode('vertical-rl')}
|
||||
>
|
||||
<MdOutlineTextRotationDown size={20} />
|
||||
<MdOutlineTextRotationDown />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center justify-around pl-4'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
@@ -86,7 +86,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
)}
|
||||
onClick={() => setActivePanel(tab)}
|
||||
>
|
||||
<Icon size={20} className='mr-0' />
|
||||
<Icon className='mr-0' />
|
||||
{window.innerWidth >= 500 ? label : ''}
|
||||
</button>
|
||||
))}
|
||||
@@ -95,7 +95,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
toggleButton={<PiDotsThreeVerticalBold />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
|
||||
@@ -4,10 +4,12 @@ import { MdInfoOutline } from 'react-icons/md';
|
||||
import { Book } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const BookCard = ({ book }: { book: Book }) => {
|
||||
const { coverImageUrl, title, author } = book;
|
||||
const _ = useTranslation();
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
|
||||
const showBookDetails = () => {
|
||||
eventDispatcher.dispatchSync('show-book-details', book);
|
||||
@@ -33,7 +35,7 @@ const BookCard = ({ book }: { book: Book }) => {
|
||||
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
|
||||
aria-label={_('More Info')}
|
||||
>
|
||||
<MdInfoOutline size={18} className='fill-base-content' onClick={showBookDetails} />
|
||||
<MdInfoOutline size={iconSize18} className='fill-base-content' onClick={showBookDetails} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import BookMenu from './BookMenu';
|
||||
|
||||
@@ -18,6 +19,8 @@ const SidebarHeader: React.FC<{
|
||||
onToggleSearchBar: () => void;
|
||||
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
|
||||
const { isTrafficLightVisible } = useTrafficLight();
|
||||
const iconSize14 = useResponsiveSize(14);
|
||||
const iconSize18 = useResponsiveSize(18);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -30,10 +33,10 @@ const SidebarHeader: React.FC<{
|
||||
onClick={onClose}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
<GiBookshelf className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex min-w-24 max-w-32 items-center justify-between sm:size-[70%]'>
|
||||
@@ -44,7 +47,7 @@ const SidebarHeader: React.FC<{
|
||||
isSearchBarVisible ? 'bg-base-300' : '',
|
||||
)}
|
||||
>
|
||||
<FiSearch size={18} className='text-base-content' />
|
||||
<FiSearch size={iconSize18} className='text-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className={clsx(
|
||||
@@ -53,7 +56,7 @@ const SidebarHeader: React.FC<{
|
||||
)}
|
||||
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'
|
||||
toggleButton={<MdOutlineMenu size={20} className='fill-base-content' />}
|
||||
toggleButton={<MdOutlineMenu className='fill-base-content' />}
|
||||
>
|
||||
<BookMenu />
|
||||
</Dropdown>
|
||||
@@ -65,7 +68,7 @@ const SidebarHeader: React.FC<{
|
||||
isPinned ? 'bg-base-300' : 'bg-base-300/65',
|
||||
)}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
{isPinned ? <MdPushPin size={iconSize14} /> : <MdOutlinePushPin size={iconSize14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import SearchOptions from './SearchOptions';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_DEFAULT = 2;
|
||||
const MINIMUM_SEARCH_TERM_LENGTH_CJK = 1;
|
||||
@@ -43,6 +44,9 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
const isSearchPending = useRef(false);
|
||||
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const iconSize12 = useResponsiveSize(12);
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchTermChange(searchTerm);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -153,7 +157,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
<div className='relative p-2'>
|
||||
<div className='bg-base-100 flex h-8 items-center rounded-lg'>
|
||||
<div className='pl-3'>
|
||||
<FaSearch className='text-gray-500' />
|
||||
<FaSearch size={iconSize16} className='text-gray-500' />
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -170,7 +174,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
|
||||
toggleButton={<FaChevronDown size={12} className='text-gray-500' />}
|
||||
toggleButton={<FaChevronDown size={iconSize12} className='text-gray-500' />}
|
||||
>
|
||||
<SearchOptions
|
||||
searchConfig={searchConfig}
|
||||
|
||||
@@ -22,7 +22,7 @@ const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isActive && <MdCheck size={20} className='text-base-content' />}
|
||||
{isActive && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>{label}</span>
|
||||
</div>
|
||||
|
||||
@@ -37,11 +37,11 @@ const TabNavigation: React.FC<{
|
||||
>
|
||||
<div className={clsx('')} onClick={() => onTabChange(tab)}>
|
||||
{tab === 'toc' ? (
|
||||
<MdToc size={20} className='mx-auto' />
|
||||
<MdToc className='mx-auto' />
|
||||
) : tab === 'annotations' ? (
|
||||
<MdEditNote size={20} className='mx-auto' />
|
||||
<MdEditNote className='mx-auto' />
|
||||
) : (
|
||||
<MdBookmarkBorder size={20} className='mx-auto' />
|
||||
<MdBookmarkBorder className='mx-auto' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdStop } from
|
||||
import { RiVoiceAiFill } from 'react-icons/ri';
|
||||
import { MdCheck } from 'react-icons/md';
|
||||
import { TTSVoice } from '@/services/tts';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
type TTSPanelProps = {
|
||||
bookKey: string;
|
||||
@@ -42,6 +43,9 @@ const TTSPanel = ({
|
||||
const [rate, setRate] = useState(viewSettings?.ttsRate ?? 1.0);
|
||||
const [selectedVoice, setSelectedVoice] = useState(viewSettings?.ttsVoice ?? '');
|
||||
|
||||
const iconSize32 = useResponsiveSize(32);
|
||||
const iconSize48 = useResponsiveSize(48);
|
||||
|
||||
const handleSetRate = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
let newRate = parseFloat(e.target.value);
|
||||
newRate = Math.max(0.2, Math.min(3.0, newRate));
|
||||
@@ -108,24 +112,24 @@ const TTSPanel = ({
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<button onClick={onBackward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastRewind size={32} />
|
||||
<MdFastRewind size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onTogglePlay} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
{isPlaying ? (
|
||||
<MdPauseCircle size={48} className='fill-primary' />
|
||||
<MdPauseCircle size={iconSize48} className='fill-primary' />
|
||||
) : (
|
||||
<MdPlayCircle size={48} className='fill-primary' />
|
||||
<MdPlayCircle size={iconSize48} className='fill-primary' />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onForward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastForward size={32} />
|
||||
<MdFastForward size={iconSize32} />
|
||||
</button>
|
||||
<button onClick={onStop} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdStop size={32} />
|
||||
<MdStop size={iconSize32} />
|
||||
</button>
|
||||
<div className='dropdown dropdown-top'>
|
||||
<button tabIndex={0} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<RiVoiceAiFill size={32} />
|
||||
<RiVoiceAiFill size={iconSize32} />
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
@@ -141,9 +145,7 @@ const TTSPanel = ({
|
||||
>
|
||||
<div className='flex items-center px-2'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{selectedVoice === voice.id && (
|
||||
<MdCheck size={20} className='text-base-content' />
|
||||
)}
|
||||
{selectedVoice === voice.id && <MdCheck className='text-base-content' />}
|
||||
</span>
|
||||
<span className={clsx('text-sm', voice.disabled && 'text-gray-400')}>
|
||||
{voice.name}
|
||||
|
||||
@@ -43,6 +43,10 @@ export const handleMousedown = (bookKey: string, event: MouseEvent) => {
|
||||
};
|
||||
|
||||
export const handleMouseup = (bookKey: string, event: MouseEvent) => {
|
||||
// we will handle mouse back and forward buttons ourselves
|
||||
if ([3, 4].includes(event.button)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'iframe-mouseup',
|
||||
|
||||
@@ -67,7 +67,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
<MdArrowBackIosNew />
|
||||
</button>
|
||||
<div className='z-15 pointer-events-none absolute inset-0 flex h-11 items-center justify-center'>
|
||||
<span className='line-clamp-1 text-center font-bold'>{title ?? ''}</span>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import { CSPostHogProvider } from '@/context/PHContext';
|
||||
import { SyncProvider } from '@/context/SyncContext';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => (
|
||||
<CSPostHogProvider>
|
||||
<EnvProvider>
|
||||
<AuthProvider>
|
||||
<SyncProvider>{children}</SyncProvider>
|
||||
</AuthProvider>
|
||||
</EnvProvider>
|
||||
</CSPostHogProvider>
|
||||
);
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const iconSize = useDefaultIconSize();
|
||||
return (
|
||||
<CSPostHogProvider>
|
||||
<EnvProvider>
|
||||
<AuthProvider>
|
||||
<IconContext.Provider value={{ size: `${iconSize}px` }}>
|
||||
<SyncProvider>{children}</SyncProvider>
|
||||
</IconContext.Provider>
|
||||
</AuthProvider>
|
||||
</EnvProvider>
|
||||
</CSPostHogProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
// use desktop size as base size
|
||||
export const useResponsiveSize = (baseSize: number) => {
|
||||
const isPhone = useMediaQuery({ maxWidth: 480 });
|
||||
const isTablet = useMediaQuery({ minWidth: 481, maxWidth: 1024 });
|
||||
if (isPhone) return baseSize * 1.5;
|
||||
if (isTablet) return baseSize * 1.25;
|
||||
return baseSize;
|
||||
};
|
||||
|
||||
export const useDefaultIconSize = () => {
|
||||
return useResponsiveSize(20);
|
||||
};
|
||||
Generated
+39
@@ -125,6 +125,9 @@ importers:
|
||||
react-icons:
|
||||
specifier: ^5.4.0
|
||||
version: 5.4.0(react@19.0.0)
|
||||
react-responsive:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(react@19.0.0)
|
||||
tinycolor2:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
@@ -1945,6 +1948,9 @@ packages:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
css-mediaquery@0.1.2:
|
||||
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
|
||||
|
||||
css-selector-tokenizer@0.8.0:
|
||||
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
|
||||
|
||||
@@ -2559,6 +2565,9 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
hyphenate-style-name@1.1.0:
|
||||
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
|
||||
|
||||
i18next-browser-languagedetector@8.0.2:
|
||||
resolution: {integrity: sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==}
|
||||
|
||||
@@ -2957,6 +2966,9 @@ packages:
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
matchmediaquery@0.4.2:
|
||||
resolution: {integrity: sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==}
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
@@ -3489,6 +3501,12 @@ packages:
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-responsive@10.0.0:
|
||||
resolution: {integrity: sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3669,6 +3687,9 @@ packages:
|
||||
resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shallow-equal@3.1.0:
|
||||
resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==}
|
||||
|
||||
sharp@0.33.5:
|
||||
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
@@ -6142,6 +6163,8 @@ snapshots:
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
css-mediaquery@0.1.2: {}
|
||||
|
||||
css-selector-tokenizer@0.8.0:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
@@ -6898,6 +6921,8 @@ snapshots:
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
hyphenate-style-name@1.1.0: {}
|
||||
|
||||
i18next-browser-languagedetector@8.0.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
@@ -7270,6 +7295,10 @@ snapshots:
|
||||
make-error@1.3.6:
|
||||
optional: true
|
||||
|
||||
matchmediaquery@0.4.2:
|
||||
dependencies:
|
||||
css-mediaquery: 0.1.2
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@@ -7743,6 +7772,14 @@ snapshots:
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-responsive@10.0.0(react@19.0.0):
|
||||
dependencies:
|
||||
hyphenate-style-name: 1.1.0
|
||||
matchmediaquery: 0.4.2
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
shallow-equal: 3.1.0
|
||||
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -7964,6 +8001,8 @@ snapshots:
|
||||
dependencies:
|
||||
kind-of: 6.0.3
|
||||
|
||||
shallow-equal@3.1.0: {}
|
||||
|
||||
sharp@0.33.5:
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
|
||||
Reference in New Issue
Block a user