ui: responsive dialog size for portrait screens (#473)

* fix: handle network error in sync API

* fix: reasonable default column width for iPad

* ui: responsive dialog size for portrait screens
This commit is contained in:
Huang Xin
2025-03-01 17:57:29 +01:00
committed by GitHub
parent 2f29295c6c
commit 2db83b8569
16 changed files with 76 additions and 46 deletions
@@ -64,7 +64,7 @@
"Match Case": "匹配大小写",
"Match Diacritics": "匹配重音符号",
"Match Whole Words": "匹配整个单词",
"Maximum Block Size": "内容最大度",
"Maximum Block Size": "内容最大度",
"Maximum Inline Size": "每栏最大宽度",
"Maximum Number of Columns": "分栏数",
"Minimum Font Size": "最小字号",
@@ -64,7 +64,7 @@
"Match Case": "匹配大小寫",
"Match Diacritics": "匹配重音符號",
"Match Whole Words": "匹配整個單詞",
"Maximum Block Size": "內容最大度",
"Maximum Block Size": "內容最大度",
"Maximum Inline Size": "每欄最大寬度",
"Maximum Number of Columns": "分欄數",
"Minimum Font Size": "最小字號",
@@ -12,7 +12,6 @@ import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { getBookDirFromWritingMode } from '@/utils/book';
import { useTheme } from '@/hooks/useTheme';
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import {
handleKeydown,
handleMousedown,
@@ -23,6 +22,7 @@ import {
handleTouchMove,
handleTouchEnd,
} from '../utils/iframeEventHandlers';
import { getMaxInlineSize } from '@/utils/config';
const FoliateViewer: React.FC<{
bookKey: string;
@@ -154,10 +154,7 @@ const FoliateViewer: React.FC<{
const gapPercent = viewSettings.gapPercent!;
const animated = viewSettings.animated!;
const maxColumnCount = viewSettings.maxColumnCount!;
const maxInlineSize =
maxColumnCount === 1 || isScrolled
? ONE_COLUMN_MAX_INLINE_SIZE
: viewSettings.maxInlineSize!;
const maxInlineSize = getMaxInlineSize(viewSettings);
const maxBlockSize = viewSettings.maxBlockSize!;
if (animated) {
view.renderer.setAttribute('animated', '');
@@ -5,17 +5,13 @@ import { BiMoon, BiSun } from 'react-icons/bi';
import { TbSunMoon } from 'react-icons/tb';
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
import {
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
ONE_COLUMN_MAX_INLINE_SIZE,
ZOOM_STEP,
} from '@/services/constants';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
import MenuItem from '@/components/MenuItem';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme, ThemeMode } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
interface ViewMenuProps {
bookKey: string;
@@ -65,7 +61,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
getView(bookKey)?.renderer.setAttribute(
'max-inline-size',
`${viewSettings.maxColumnCount === 1 || isScrolledMode ? ONE_COLUMN_MAX_INLINE_SIZE : viewSettings.maxInlineSize}px`,
`${getMaxInlineSize(viewSettings)}px`,
);
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
viewSettings!.scrolled = isScrolledMode;
@@ -17,9 +17,10 @@ import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { useEnv } from '@/context/EnvContext';
import { getStyles } from '@/utils/style';
import { getOSPlatform } from '@/utils/misc';
import { FONT_ENUM_SUPPORTED_OS_PLATFORMS, getSysFontsList } from '@/utils/font';
import { getSysFontsList } from '@/utils/font';
import { isTauriAppPlatform } from '@/services/environment';
interface FontFaceProps {
@@ -60,6 +61,7 @@ const FontFace = ({
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const view = getView(bookKey);
@@ -109,7 +111,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [fontWeight, setFontWeight] = useState(viewSettings.fontWeight!);
useEffect(() => {
if (isTauriAppPlatform() && FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
if (isTauriAppPlatform() && appService?.hasSysFontsList) {
getSysFontsList().then((fonts) => {
setSysFonts(fonts);
});
@@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react';
import { MdOutlineAutoMode } from 'react-icons/md';
import { MdOutlineTextRotationDown, MdOutlineTextRotationNone } from 'react-icons/md';
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import NumberInput from './NumberInput';
@@ -147,10 +147,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setSettings(settings);
}
view?.renderer.setAttribute('max-column-count', maxColumnCount);
view?.renderer.setAttribute(
'max-inline-size',
`${maxColumnCount === 1 || viewSettings.scrolled ? ONE_COLUMN_MAX_INLINE_SIZE : maxInlineSize}px`,
);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxColumnCount]);
@@ -161,10 +158,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
settings.globalViewSettings.maxInlineSize = maxInlineSize;
setSettings(settings);
}
view?.renderer.setAttribute(
'max-inline-size',
`${maxColumnCount === 1 || viewSettings.scrolled ? ONE_COLUMN_MAX_INLINE_SIZE : maxInlineSize}px`,
);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxInlineSize]);
@@ -353,6 +347,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
label={_('Maximum Inline Size')}
value={maxInlineSize}
onChange={setMaxInlineSize}
disabled={maxColumnCount === 1 || viewSettings.scrolled}
min={500}
max={9999}
step={100}
@@ -9,6 +9,7 @@ interface NumberInputProps {
min: number;
max: number;
step?: number;
disabled?: boolean;
onChange: (value: number) => void;
}
@@ -20,6 +21,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
min,
max,
step,
disabled,
}) => {
const [localValue, setLocalValue] = useState(value);
const numberStep = step || 1;
@@ -74,13 +76,13 @@ const NumberInput: React.FC<NumberInputProps> = ({
/>
<button
onClick={decrement}
className={`btn btn-circle btn-sm ${value === min ? 'btn-disabled !bg-opacity-5' : ''}`}
className={`btn btn-circle btn-sm ${value <= min || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
<FiMinus className='h-4 w-4' />
</button>
<button
onClick={increment}
className={`btn btn-circle btn-sm ${value === max ? 'btn-disabled !bg-opacity-5' : ''}`}
className={`btn btn-circle btn-sm ${value >= max || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
<FiPlus className='h-4 w-4' />
</button>
@@ -64,7 +64,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName='sm:w-1/2 sm:min-w-[480px] sm:h-[65%]'
boxClassName='sm:min-w-[480px]'
header={
<div className='flex w-full items-center justify-between'>
<button
+4 -1
View File
@@ -110,7 +110,10 @@ const Dialog: React.FC<DialogProps> = ({
<div
className={clsx(
'modal-box settings-content z-20 flex flex-col rounded-none rounded-tl-2xl rounded-tr-2xl p-0 sm:rounded-2xl',
'h-full max-h-full w-full max-w-full sm:w-[65%] sm:max-w-[600px]',
'h-full max-h-full w-full max-w-full',
window.innerWidth < window.innerHeight
? 'sm:h-[50%] sm:w-3/4'
: 'sm:h-[65%] sm:w-1/2 sm:max-w-[600px]',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)] sm:pt-0',
boxClassName,
)}
+20 -13
View File
@@ -30,19 +30,26 @@ const getUserAndToken = async (req: NextRequest) => {
if (!authHeader) return {};
const token = authHeader.replace('Bearer ', '');
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error || !user) return {};
return { user, token };
try {
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error?.message === 'fetch failed') {
return { error: 'Network error' };
} else if (error || !user) {
return { error: 'Not authenticated' };
}
return { user, token };
} catch {
return { error: 'Network error' };
}
};
export async function GET(req: NextRequest) {
const { user, token } = await getUserAndToken(req);
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
const { user, token, error } = await getUserAndToken(req);
if (!user || !token || error) {
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
}
const supabase = createSupabaseClient(token);
@@ -114,9 +121,9 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const { user, token } = await getUserAndToken(req);
if (!user || !token) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
const { user, token, error } = await getUserAndToken(req);
if (!user || !token || error) {
return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 });
}
const supabase = createSupabaseClient(token);
@@ -53,6 +53,7 @@ export abstract class BaseAppService implements AppService {
abstract hasRoundedWindow: boolean;
abstract hasSafeAreaInset: boolean;
abstract hasHaptics: boolean;
abstract hasSysFontsList: boolean;
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
abstract getCoverImageUrl(book: Book): string;
+3 -4
View File
@@ -10,6 +10,7 @@ import {
} from '@/types/book';
import { ReadSettings, SystemSettings } from '@/types/settings';
import { UserStorageQuota } from '@/types/user';
import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from '@/utils/config';
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
@@ -62,8 +63,8 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
scrolled: false,
disableClick: false,
maxColumnCount: 2,
maxInlineSize: 720,
maxBlockSize: 1440,
maxInlineSize: getDefaultMaxInlineSize(),
maxBlockSize: getDefaultMaxBlockSize(),
animated: false,
writingMode: 'auto',
vertical: false,
@@ -381,8 +382,6 @@ export const ANDROID_FONTS = [
'XiHeiti',
];
export const ONE_COLUMN_MAX_INLINE_SIZE = 9999;
export const BOOK_IDS_SEPARATOR = '+';
export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web';
@@ -127,6 +127,7 @@ export class NativeAppService extends BaseAppService {
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
hasSysFontsList = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
@@ -189,6 +189,7 @@ export class WebAppService extends BaseAppService {
hasRoundedWindow = false;
hasSafeAreaInset = isPWA();
hasHaptics = false;
hasSysFontsList = false;
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
+1
View File
@@ -30,6 +30,7 @@ export interface AppService {
hasRoundedWindow: boolean;
hasSafeAreaInset: boolean;
hasHaptics: boolean;
hasSysFontsList: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
isAndroidApp: boolean;
+25
View File
@@ -0,0 +1,25 @@
import { ViewSettings } from '@/types/book';
export const getMaxInlineSize = (viewSettings: ViewSettings) => {
const isScrolled = viewSettings.scrolled!;
const maxColumnCount = viewSettings.maxColumnCount!;
const screenWidth = window.innerWidth;
return maxColumnCount === 1 || isScrolled ? screenWidth : viewSettings.maxInlineSize!;
};
export const getDefaultMaxInlineSize = () => {
if (typeof window === 'undefined') return 720;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return screenWidth < screenHeight ? screenWidth : 720;
};
export const getDefaultMaxBlockSize = () => {
if (typeof window === 'undefined') return 1440;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
return Math.max(screenWidth, screenHeight);
};