forked from akai/readest
mobile: support safe area insets for mobile platforms (#312)
This commit is contained in:
@@ -50,6 +50,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 h-11 w-full py-2 pr-6',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -353,7 +353,7 @@ const LibraryPage = () => {
|
||||
<div
|
||||
className={clsx(
|
||||
'library-page bg-base-200 text-base-content flex h-dvh select-none flex-col overflow-hidden',
|
||||
isTauriAppPlatform() && 'rounded-window',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
@@ -370,7 +370,13 @@ const LibraryPage = () => {
|
||||
)}
|
||||
{libraryLoaded &&
|
||||
(libraryBooks.length > 0 ? (
|
||||
<div ref={containerRef} className='mt-12 flex-grow overflow-auto px-2'>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'mt-12 flex-grow overflow-auto px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
|
||||
)}
|
||||
>
|
||||
<Suspense>
|
||||
<Bookshelf
|
||||
libraryBooks={libraryBooks}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
@@ -23,6 +24,7 @@ interface BooksGridProps {
|
||||
}
|
||||
|
||||
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
const { appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { getProgress, getViewState, getViewSettings } = useReaderStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
@@ -39,7 +41,10 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='grid h-full flex-grow'
|
||||
className={clsx(
|
||||
'grid h-full flex-grow',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
style={{
|
||||
gridTemplateColumns: gridTemplate.columns,
|
||||
gridTemplateRows: gridTemplate.rows,
|
||||
@@ -61,7 +66,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
id={`gridcell-${bookKey}`}
|
||||
key={bookKey}
|
||||
className={`${isTauriAppPlatform() ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
|
||||
className={`${appService?.hasRoundedWindow ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
|
||||
>
|
||||
{isBookmarked && <Ribbon width={marginGap} />}
|
||||
<HeaderBar
|
||||
|
||||
@@ -4,11 +4,11 @@ import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
|
||||
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isPWA, isTauriAppPlatform } from '@/services/environment';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
interface FooterBarProps {
|
||||
@@ -19,6 +19,7 @@ interface FooterBarProps {
|
||||
|
||||
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { hoveredBookKey, setHoveredBookKey, getView, getProgress } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const view = getView(bookKey);
|
||||
@@ -62,9 +63,9 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
className={clsx(
|
||||
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
|
||||
'shadow-xs bg-base-100 transition-opacity duration-300',
|
||||
isPWA() ? 'pb-[env(safe-area-inset-bottom)]' : '',
|
||||
isTauriAppPlatform() && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-bottom-left',
|
||||
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
|
||||
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
@@ -53,8 +52,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
`header-bar absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
|
||||
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
|
||||
`shadow-xs bg-base-100 transition-opacity duration-300`,
|
||||
isTauriAppPlatform() && 'rounded-window-top-right',
|
||||
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-top-left',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
|
||||
isDropdownOpen && 'header-bar-pinned',
|
||||
|
||||
@@ -8,13 +8,12 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import ReaderContent from './ReaderContent';
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { library, setLibrary } = useLibraryStore();
|
||||
const isInitiating = useRef(false);
|
||||
@@ -42,7 +41,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
<div
|
||||
className={clsx(
|
||||
`reader-page bg-base-100 text-base-content select-none`,
|
||||
isTauriAppPlatform() && 'rounded-window',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
)}
|
||||
>
|
||||
<Suspense>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
@@ -25,7 +24,7 @@ const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
const Notebook: React.FC = ({}) => {
|
||||
const _ = useTranslation();
|
||||
const { updateAppTheme } = useTheme();
|
||||
const { envConfig } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
const { notebookWidth, isNotebookVisible, isNotebookPinned } = useNotebookStore();
|
||||
@@ -152,7 +151,7 @@ const Notebook: React.FC = ({}) => {
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
|
||||
isTauriAppPlatform() && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
|
||||
!isNotebookPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -2,16 +2,17 @@ import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import TOCView from './TOCView';
|
||||
import BooknoteView from './BooknoteView';
|
||||
import TabNavigation from './TabNavigation';
|
||||
import { isPWA } from '@/services/environment';
|
||||
|
||||
const SidebarContent: React.FC<{
|
||||
bookDoc: BookDoc;
|
||||
sideBarBookKey: string;
|
||||
}> = ({ bookDoc, sideBarBookKey }) => {
|
||||
const { appService } = useEnv();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
const config = getConfig(sideBarBookKey);
|
||||
@@ -92,7 +93,10 @@ const SidebarContent: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx('flex-shrink-0', isPWA() ? 'pb-[calc(env(safe-area-inset-bottom)/2)]' : '')}
|
||||
className={clsx(
|
||||
'flex-shrink-0',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)/2)]',
|
||||
)}
|
||||
>
|
||||
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { BookSearchResult } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
@@ -24,6 +24,7 @@ const MAX_SIDEBAR_WIDTH = 0.45;
|
||||
const SideBar: React.FC<{
|
||||
onGoToLibrary: () => void;
|
||||
}> = ({ onGoToLibrary }) => {
|
||||
const { appService } = useEnv();
|
||||
const { updateAppTheme } = useTheme();
|
||||
const { settings } = useSettingsStore();
|
||||
const { sideBarBookKey } = useSidebarStore();
|
||||
@@ -118,7 +119,8 @@ const SideBar: React.FC<{
|
||||
<div
|
||||
className={clsx(
|
||||
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 select-none flex-col',
|
||||
isTauriAppPlatform() && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
|
||||
!isSideBarPinned && 'shadow-2xl',
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -10,7 +11,6 @@ import { eventDispatcher } from '@/utils/event';
|
||||
import { parseSSMLLang } from '@/utils/ssml';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { throttle } from '@/utils/throttle';
|
||||
import { isPWA } from '@/services/environment';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
@@ -21,6 +21,7 @@ const POPUP_PADDING = 10;
|
||||
|
||||
const TTSControl = () => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const [bookKey, setBookKey] = useState<string>('');
|
||||
@@ -274,7 +275,9 @@ const TTSControl = () => {
|
||||
ref={iconRef}
|
||||
className={clsx(
|
||||
'absolute right-6 h-12 w-12',
|
||||
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]' : 'bottom-12',
|
||||
appService?.hasSafeAreaInset
|
||||
? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]'
|
||||
: 'bottom-12',
|
||||
)}
|
||||
>
|
||||
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
|
||||
|
||||
@@ -37,14 +37,19 @@ import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/l
|
||||
import { ProgressHandler } from '@/utils/transfer';
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
osPlatform: string = getOSPlatform();
|
||||
isMobile: boolean = ['android', 'ios'].includes(getOSPlatform());
|
||||
localBooksDir: string = '';
|
||||
abstract fs: FileSystem;
|
||||
abstract appPlatform: AppPlatform;
|
||||
abstract isAppDataSandbox: boolean;
|
||||
abstract isAndroidApp: boolean;
|
||||
abstract isIOSApp: boolean;
|
||||
abstract hasTrafficLight: boolean;
|
||||
abstract hasWindowBar: boolean;
|
||||
abstract hasContextMenu: boolean;
|
||||
abstract hasRoundedWindow: boolean;
|
||||
abstract hasSafeAreaInset: boolean;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
|
||||
@@ -22,8 +22,9 @@ import { isValidURL } from '@/utils/misc';
|
||||
|
||||
import { BaseAppService } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
import { isPWA } from './environment';
|
||||
|
||||
export const isMobile = ['android', 'ios'].includes(osType());
|
||||
const OS_TYPE = osType();
|
||||
|
||||
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
switch (base) {
|
||||
@@ -117,10 +118,14 @@ export const nativeFileSystem: FileSystem = {
|
||||
export class NativeAppService extends BaseAppService {
|
||||
fs = nativeFileSystem;
|
||||
appPlatform = 'tauri' as AppPlatform;
|
||||
isAppDataSandbox = isMobile;
|
||||
hasTrafficLight = osType() === 'macos';
|
||||
hasWindowBar = !(osType() === 'ios' || osType() === 'android');
|
||||
hasContextMenu = !(osType() === 'ios' || osType() === 'android');
|
||||
isAppDataSandbox = ['android', 'ios'].includes(OS_TYPE);
|
||||
isAndroidApp = OS_TYPE === 'android';
|
||||
isIOSApp = OS_TYPE === 'ios';
|
||||
hasTrafficLight = OS_TYPE === 'macos';
|
||||
hasWindowBar = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasContextMenu = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android' || isPWA();
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -179,9 +179,13 @@ export class WebAppService extends BaseAppService {
|
||||
fs = indexedDBFileSystem;
|
||||
appPlatform = 'web' as AppPlatform;
|
||||
isAppDataSandbox = false;
|
||||
isAndroidApp = false;
|
||||
isIOSApp = false;
|
||||
hasTrafficLight = false;
|
||||
hasWindowBar = false;
|
||||
hasContextMenu = false;
|
||||
hasRoundedWindow = false;
|
||||
hasSafeAreaInset = false;
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -22,12 +22,17 @@ export interface FileSystem {
|
||||
|
||||
export interface AppService {
|
||||
fs: FileSystem;
|
||||
osPlatform: string;
|
||||
appPlatform: AppPlatform;
|
||||
hasTrafficLight: boolean;
|
||||
hasWindowBar: boolean;
|
||||
hasContextMenu: boolean;
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
isAndroidApp: boolean;
|
||||
isIOSApp: boolean;
|
||||
|
||||
selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user