fix: RTL layout for the bottom configuration panel, closes #849 (#850)

This commit is contained in:
Huang Xin
2025-04-09 16:47:15 +08:00
committed by GitHub
parent ff4d8af054
commit 3d2847e427
10 changed files with 82 additions and 28 deletions
+2 -1
View File
@@ -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>
+11 -4
View File
@@ -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>
+29 -5
View File
@@ -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%',
}}
>
+7 -8
View File
@@ -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);
};
+7 -3
View File
@@ -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;
};
+13
View File
@@ -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);
};