diff --git a/README.md b/README.md index ba15d6ee..37f0dc2d 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 6079e3c2..4d78b9a3 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -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); diff --git a/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx b/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx index 0af5122e..efed82e7 100644 --- a/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx +++ b/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx @@ -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( (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' } > - + {isRtl ? : }
{tabConfig.map(({ tab, icon: Icon, label }) => ( diff --git a/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx b/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx index 6b17820b..aa6bc35a 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx @@ -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) => ( diff --git a/apps/readest-app/src/components/AboutWindow.tsx b/apps/readest-app/src/components/AboutWindow.tsx index 8698e2c1..019c23f2 100644 --- a/apps/readest-app/src/components/AboutWindow.tsx +++ b/apps/readest-app/src/components/AboutWindow.tsx @@ -56,7 +56,7 @@ export const AboutWindow = () => {
-
+

© {new Date().getFullYear()} Bilingify LLC. All rights reserved.

diff --git a/apps/readest-app/src/components/Dialog.tsx b/apps/readest-app/src/components/Dialog.tsx index 8467ae8e..763e9ba1 100644 --- a/apps/readest-app/src/components/Dialog.tsx +++ b/apps/readest-app/src/components/Dialog.tsx @@ -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 = ({ }) => { 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 = ({ '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} >
= ({ 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' } > - + {isRtl ? ( + + ) : ( + + )}
{title ?? ''} diff --git a/apps/readest-app/src/components/Slider.tsx b/apps/readest-app/src/components/Slider.tsx index aed5bf88..176d3e3a 100644 --- a/apps/readest-app/src/components/Slider.tsx +++ b/apps/readest-app/src/components/Slider.tsx @@ -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 = ({ onChange, }) => { const [value, setValue] = useState(initialValue); + const [isRtl, setIsRtl] = useState(false); + const sliderRef = useRef(null); const handleChange = (e: React.ChangeEvent) => { const newValue = parseInt((e.target as HTMLInputElement).value, 10); @@ -43,6 +45,17 @@ const Slider: React.FC = ({ } }; + 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 = ({ const percentage = ((value - min) / (max - min)) * 100; return ( -
+
+ {/* Background track */}
+ {/* Filled portion */}
0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px' }} + style={{ + width: percentage > 0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px', + [isRtl ? 'right' : 'left']: 0, + }} >
+ {/* Min/Max labels */}
{minLabel} {maxLabel}
+ {/* Thumb bubble */}
diff --git a/apps/readest-app/src/utils/book.ts b/apps/readest-app/src/utils/book.ts index 24aa316e..88f0ea20 100644 --- a/apps/readest-app/src/utils/book.ts +++ b/apps/readest-app/src/utils/book.ts @@ -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); }; diff --git a/apps/readest-app/src/utils/misc.ts b/apps/readest-app/src/utils/misc.ts index 0059801b..4e2394be 100644 --- a/apps/readest-app/src/utils/misc.ts +++ b/apps/readest-app/src/utils/misc.ts @@ -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; }; diff --git a/apps/readest-app/src/utils/rtl.ts b/apps/readest-app/src/utils/rtl.ts new file mode 100644 index 00000000..9dd37e25 --- /dev/null +++ b/apps/readest-app/src/utils/rtl.ts @@ -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); +};