forked from akai/readest
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface AppService {
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
hasHaptics: boolean;
|
||||
hasSysFontsList: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
isAndroidApp: boolean;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user