forked from akai/readest
@@ -252,9 +252,10 @@ The following libraries and frameworks are used in this software:
|
||||
- [zip.js](https://github.com/gildas-lormeau/zip.js), which is licensed under the BSD-3-Clause license.
|
||||
- [fflate](https://github.com/101arrowz/fflate), which is MIT licensed.
|
||||
- [PDF.js](https://github.com/mozilla/pdf.js), which is licensed under Apache License 2.0.
|
||||
- [daisyUI](https://github.com/saadeghi/daisyui), which is MIT licensed.
|
||||
- [next.js](https://github.com/vercel/next.js), which is MIT licensed.
|
||||
- [react](https://github.com/facebook/react), which is MIT licensed.
|
||||
- [react-icons](https://github.com/react-icons/react-icons), which has various open-source licenses.
|
||||
- [react](https://github.com/facebook/react), which is MIT licensed.
|
||||
- [tauri](https://github.com/tauri-apps/tauri), which is MIT licensed.
|
||||
|
||||
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
handleTouchEnd,
|
||||
} from '../utils/iframeEventHandlers';
|
||||
import { getMaxInlineSize } from '@/utils/config';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
|
||||
const FoliateViewer: React.FC<{
|
||||
@@ -84,7 +85,11 @@ const FoliateViewer: React.FC<{
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
viewSettings.vertical =
|
||||
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
|
||||
viewSettings.rtl = writingDir?.rtl || viewSettings.writingMode.includes('rl') || false;
|
||||
viewSettings.rtl =
|
||||
writingDir?.rtl ||
|
||||
getDirFromUILanguage() === 'rtl' ||
|
||||
viewSettings.writingMode.includes('rl') ||
|
||||
false;
|
||||
setViewSettings(bookKey, { ...viewSettings });
|
||||
|
||||
mountAdditionalFonts(detail.doc);
|
||||
|
||||
@@ -8,8 +8,8 @@ import { RiDashboardLine } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { IoAccessibilityOutline } from 'react-icons/io5';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import { MdArrowBackIosNew, MdArrowForwardIos } from 'react-icons/md';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import FontPanel from './FontPanel';
|
||||
import LayoutPanel from './LayoutPanel';
|
||||
import ColorPanel from './ColorPanel';
|
||||
@@ -28,6 +28,7 @@ type TabConfig = {
|
||||
|
||||
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
|
||||
const [activePanel, setActivePanel] = useState<SettingsPanelType>(
|
||||
(localStorage.getItem('lastConfigPanel') || 'Font') as SettingsPanelType,
|
||||
);
|
||||
@@ -79,10 +80,10 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
tabIndex={-1}
|
||||
onClick={handleClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew />
|
||||
{isRtl ? <MdArrowForwardIos /> : <MdArrowBackIosNew />}
|
||||
</button>
|
||||
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center gap-2 pl-4'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
|
||||
@@ -249,7 +249,7 @@ const TTSPanel = ({
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 no-triangle menu menu-vertical rounded-box absolute right-0 z-[1] shadow',
|
||||
'mt-4 inline max-h-96 w-[250px] overflow-y-scroll',
|
||||
'mt-4 inline max-h-96 w-[200px] overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{timeoutOptions.map((option, index) => (
|
||||
|
||||
@@ -56,7 +56,7 @@ export const AboutWindow = () => {
|
||||
|
||||
<div className='divider py-12 sm:py-2'></div>
|
||||
|
||||
<div className='flex flex-col items-center px-4 text-center'>
|
||||
<div className='flex flex-col items-center px-4 text-center' dir='ltr'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { MdArrowBackIosNew, MdArrowForwardIos } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
|
||||
const VELOCITY_THRESHOLD = 0.5;
|
||||
const SNAP_THRESHOLD = 0.2;
|
||||
@@ -38,6 +39,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const [isFullHeightInMobile, setIsFullHeightInMobile] = React.useState(!snapHeight);
|
||||
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
|
||||
@@ -133,6 +135,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
'modal sm:min-w-90 z-50 h-full w-full !items-start !bg-transparent sm:w-full sm:!items-center',
|
||||
className,
|
||||
)}
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<div
|
||||
className={clsx('overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20', bgClassName)}
|
||||
@@ -177,10 +180,14 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
tabIndex={-1}
|
||||
onClick={onClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={iconSize22} />
|
||||
{isRtl ? (
|
||||
<MdArrowForwardIos size={iconSize22} />
|
||||
) : (
|
||||
<MdArrowBackIosNew size={iconSize22} />
|
||||
)}
|
||||
</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,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SliderProps {
|
||||
min?: number;
|
||||
@@ -34,6 +34,8 @@ const Slider: React.FC<SliderProps> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const [isRtl, setIsRtl] = useState(false);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent) => {
|
||||
const newValue = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
@@ -43,6 +45,17 @@ const Slider: React.FC<SliderProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let node: HTMLElement | null = sliderRef.current;
|
||||
while (node) {
|
||||
if (node.getAttribute('dir') === 'rtl') {
|
||||
setIsRtl(true);
|
||||
break;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
@@ -50,22 +63,33 @@ const Slider: React.FC<SliderProps> = ({
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
return (
|
||||
<div className={`slider bg-base-200 mx-auto w-full max-w-md rounded-xl ${className}`}>
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={`slider bg-base-200 mx-auto w-full max-w-md rounded-xl ${className}`}
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<div className='relative' style={{ height: `${heightPx}px` }}>
|
||||
{/* Background track */}
|
||||
<div className='bg-base-300/40 absolute h-full w-full rounded-full'></div>
|
||||
{/* Filled portion */}
|
||||
<div
|
||||
className='bg-base-300 absolute h-full rounded-full'
|
||||
style={{ width: percentage > 0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px' }}
|
||||
style={{
|
||||
width: percentage > 0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px',
|
||||
[isRtl ? 'right' : 'left']: 0,
|
||||
}}
|
||||
></div>
|
||||
{/* Min/Max labels */}
|
||||
<div className='absolute inset-0 flex items-center justify-between px-4 text-sm'>
|
||||
<span className={`ml-2 ${minClassName}`}>{minLabel}</span>
|
||||
<span className={`mr-2 ${maxClassName}`}>{maxLabel}</span>
|
||||
</div>
|
||||
{/* Thumb bubble */}
|
||||
<div
|
||||
className='pointer-events-none absolute top-0 z-10'
|
||||
style={{
|
||||
left: `max(${heightPx / 2}px, calc(${percentage}%))`,
|
||||
transform: 'translateX(calc(-50%))',
|
||||
[isRtl ? 'right' : 'left']: `max(${heightPx / 2}px, calc(${percentage}%))`,
|
||||
transform: isRtl ? 'translateX(calc(50%))' : 'translateX(calc(-50%))',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EXTS } from '@/libs/document';
|
||||
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, isContentURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
import { getDirFromLanguage } from './rtl';
|
||||
|
||||
export const getDir = (book: Book) => {
|
||||
return `${book.hash}`;
|
||||
@@ -57,16 +58,16 @@ export interface Contributor {
|
||||
name: LanguageMap;
|
||||
}
|
||||
|
||||
const userLang = getUserLang();
|
||||
|
||||
const formatLanguageMap = (x: string | LanguageMap): string => {
|
||||
const userLang = getUserLang();
|
||||
if (!x) return '';
|
||||
if (typeof x === 'string') return x;
|
||||
const keys = Object.keys(x);
|
||||
return x[userLang] || x[keys[0]!]!;
|
||||
};
|
||||
|
||||
export const listFormater = (narrow = false, lang = userLang) => {
|
||||
export const listFormater = (narrow = false, lang = '') => {
|
||||
lang = lang ? lang : getUserLang();
|
||||
if (narrow) {
|
||||
return new Intl.ListFormat('en', { style: 'narrow', type: 'unit' });
|
||||
} else {
|
||||
@@ -116,6 +117,7 @@ export const primaryLanguage = (lang: string | string[] | undefined) => {
|
||||
|
||||
export const formatDate = (date: string | number | Date | undefined) => {
|
||||
if (!date) return;
|
||||
const userLang = getUserLang();
|
||||
try {
|
||||
return new Date(date).toLocaleDateString(userLang, {
|
||||
year: 'numeric',
|
||||
@@ -157,9 +159,6 @@ export const getBookDirFromWritingMode = (writingMode: WritingMode) => {
|
||||
};
|
||||
|
||||
export const getBookDirFromLanguage = (language: string | string[] | undefined) => {
|
||||
const lang = primaryLanguage(language);
|
||||
if (!lang) return 'auto';
|
||||
const rtlLanguages = new Set(['ar', 'he', 'fa', 'ur', 'dv', 'ps', 'sd', 'yi']);
|
||||
const primaryLang = lang.split('-')[0]!.toLowerCase();
|
||||
return rtlLanguages.has(primaryLang) ? 'rtl' : 'auto';
|
||||
const lang = primaryLanguage(language) || '';
|
||||
return getDirFromLanguage(lang);
|
||||
};
|
||||
|
||||
@@ -30,12 +30,16 @@ export const makeSafeFilename = (filename: string, replacement = '_') => {
|
||||
return safeName.trim();
|
||||
};
|
||||
|
||||
export const getUserLang = () => navigator?.language.split('-')[0] || 'en';
|
||||
export const getUserLang = () => {
|
||||
const locale = localStorage?.getItem('i18nextLng') || navigator?.language || '';
|
||||
return locale.split('-')[0] || 'en';
|
||||
};
|
||||
|
||||
export const isCJKEnv = () => {
|
||||
const isCJKLocale = ['zh', 'ja', 'ko'].includes(getUserLang());
|
||||
const uiLanguage = localStorage.getItem('i18nextLng') || '';
|
||||
const browserLanguage = navigator.language || '';
|
||||
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
|
||||
const isCJKUI = ['zh', 'ja', 'ko'].some((lang) => uiLanguage.startsWith(lang));
|
||||
const isCJKLocale = ['zh', 'ja', 'ko'].some((lang) => browserLanguage.startsWith(lang));
|
||||
return isCJKLocale || isCJKUI;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getUserLang } from './misc';
|
||||
|
||||
export const getDirFromLanguage = (lang: string) => {
|
||||
if (!lang) return 'auto';
|
||||
const rtlLanguages = new Set(['ar', 'he', 'fa', 'ur', 'dv', 'ps', 'sd', 'yi']);
|
||||
const primaryLang = lang.split('-')[0]!.toLowerCase();
|
||||
return rtlLanguages.has(primaryLang) ? 'rtl' : 'auto';
|
||||
};
|
||||
|
||||
export const getDirFromUILanguage = () => {
|
||||
const lang = getUserLang();
|
||||
return getDirFromLanguage(lang);
|
||||
};
|
||||
Reference in New Issue
Block a user