feat(ux): improve pull-down interaction for mobile modal (#440)
* Adjusts overlay background opacity dynamically based on drag position * Ensures smooth transition as the modal is pulled down * Enhances UX for mobile users by making dismissal more intuitive
This commit is contained in:
@@ -49,7 +49,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 flex h-11 w-full items-center py-2 pr-4 sm:pr-6',
|
||||
'titlebar h-13 z-10 flex w-full items-center py-2 pr-4 sm:pr-6',
|
||||
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
|
||||
)}
|
||||
@@ -68,7 +68,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<div className='relative flex h-7 w-full items-center'>
|
||||
<div className='relative flex h-9 w-full items-center sm:h-7'>
|
||||
<span className='absolute left-3 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
@@ -77,7 +77,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
placeholder={_('Search Books...')}
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
'input rounded-badge bg-base-300/50 h-7 w-full pl-10 pr-10',
|
||||
'input rounded-badge bg-base-300/50 h-9 w-full pl-10 pr-10 sm:h-7',
|
||||
'font-sans text-sm font-light',
|
||||
'border-none focus:outline-none focus:ring-0',
|
||||
)}
|
||||
|
||||
@@ -346,14 +346,14 @@ const LibraryPage = () => {
|
||||
};
|
||||
|
||||
const handleToggleSelectMode = () => {
|
||||
if (!isSelectMode && appService?.isMobile) {
|
||||
if (!isSelectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode((pre) => !pre);
|
||||
};
|
||||
|
||||
const handleSetSelectMode = (selectMode: boolean) => {
|
||||
if (selectMode && appService?.isMobile) {
|
||||
if (selectMode && appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
setIsSelectMode(selectMode);
|
||||
@@ -402,8 +402,9 @@ const LibraryPage = () => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
'mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
'scroll-container mt-12 flex-grow overflow-auto px-4 sm:px-2',
|
||||
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
|
||||
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
|
||||
)}
|
||||
>
|
||||
<Bookshelf
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { TbLayoutSidebar, TbLayoutSidebarFilled } from 'react-icons/tb';
|
||||
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Button from '@/components/Button';
|
||||
@@ -12,6 +13,7 @@ interface SidebarTogglerProps {
|
||||
const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { sideBarBookKey, isSideBarVisible, setSideBarBookKey, toggleSideBar } = useSidebarStore();
|
||||
const { setHoveredBookKey } = useReaderStore();
|
||||
const handleToggleSidebar = () => {
|
||||
if (sideBarBookKey === bookKey) {
|
||||
toggleSideBar();
|
||||
@@ -19,6 +21,7 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
setSideBarBookKey(bookKey);
|
||||
if (!isSideBarVisible) toggleSideBar();
|
||||
}
|
||||
setHoveredBookKey('');
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { TextSelection } from '@/utils/sel';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
@@ -81,12 +81,6 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = 1 - e.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const handleSaveNote = (selection: TextSelection, note: string) => {
|
||||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
@@ -132,7 +126,13 @@ const Notebook: React.FC = ({}) => {
|
||||
setNotebookEditAnnotation(null);
|
||||
};
|
||||
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
const onDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = 1 - data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_NOTEBOOK_WIDTH, Math.min(MAX_NOTEBOOK_WIDTH, widthFraction));
|
||||
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(onDragMove);
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
@@ -175,7 +175,7 @@ const Notebook: React.FC = ({}) => {
|
||||
`}</style>
|
||||
<div
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleDragStart}
|
||||
/>
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
|
||||
@@ -64,6 +64,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
/>
|
||||
}
|
||||
label={book.title}
|
||||
labelClass='max-w-36'
|
||||
onClick={() => handleParallelView(book.hash)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { BookSearchResult } from '@/types/book';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import SidebarHeader from './Header';
|
||||
import SidebarContent from './Content';
|
||||
import BookCard from './BookCard';
|
||||
import useSidebar from '../../hooks/useSidebar';
|
||||
import useDragBar from '../../hooks/useDragBar';
|
||||
import SearchBar from './SearchBar';
|
||||
import SearchResults from './SearchResults';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
@@ -33,6 +33,7 @@ const SideBar: React.FC<{
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const isMobile = window.innerWidth < 640;
|
||||
const {
|
||||
sideBarWidth,
|
||||
isSideBarPinned,
|
||||
@@ -42,7 +43,7 @@ const SideBar: React.FC<{
|
||||
handleSideBarTogglePin,
|
||||
} = useSidebar(
|
||||
settings.globalReadSettings.sideBarWidth,
|
||||
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
|
||||
isMobile ? false : settings.globalReadSettings.isSideBarPinned,
|
||||
);
|
||||
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
@@ -79,12 +80,52 @@ const SideBar: React.FC<{
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
const widthFraction = e.clientX / window.innerWidth;
|
||||
const handleVerticalDragMove = (data: { clientY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const heightFraction = data.clientY / window.innerHeight;
|
||||
const newTop = Math.max(0.0, Math.min(1, heightFraction));
|
||||
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (sidebar && overlay) {
|
||||
sidebar.style.top = `${newTop * 100}%`;
|
||||
overlay.style.opacity = `${1 - heightFraction}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerticalDragEnd = (velocity: number) => {
|
||||
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
|
||||
if (!sidebar || !overlay) return;
|
||||
|
||||
if (velocity > 0.5) {
|
||||
sidebar.style.transition = `top ${0.15 / velocity}s ease-out`;
|
||||
sidebar.style.top = '100%';
|
||||
overlay.style.transition = `opacity ${0.15 / velocity}s ease-out`;
|
||||
overlay.style.opacity = '0';
|
||||
setTimeout(() => setSideBarVisible(false), 300);
|
||||
} else {
|
||||
sidebar.style.transition = 'top 0.3s ease-out';
|
||||
sidebar.style.top = '0%';
|
||||
overlay.style.transition = 'opacity 0.3s ease-out';
|
||||
overlay.style.opacity = '0.8';
|
||||
}
|
||||
};
|
||||
|
||||
const handleHorizontalDragMove = (data: { clientX: number }) => {
|
||||
const widthFraction = data.clientX / window.innerWidth;
|
||||
const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, widthFraction));
|
||||
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
|
||||
};
|
||||
const { handleMouseDown } = useDragBar(handleDragMove);
|
||||
|
||||
const { handleDragStart: handleVerticalDragStart } = useDrag(
|
||||
handleVerticalDragMove,
|
||||
handleVerticalDragEnd,
|
||||
);
|
||||
const { handleDragStart: handleHorizontalDragStart } = useDrag(handleHorizontalDragMove);
|
||||
|
||||
const handleClickOverlay = () => {
|
||||
setSideBarVisible(false);
|
||||
@@ -135,10 +176,27 @@ const SideBar: React.FC<{
|
||||
.sidebar-container {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
.sidebar-container.open {
|
||||
top: 0%;
|
||||
}
|
||||
.overlay {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex-shrink-0'>
|
||||
{isMobile && (
|
||||
<div
|
||||
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
|
||||
onMouseDown={handleVerticalDragStart}
|
||||
onTouchStart={handleVerticalDragStart}
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
<SidebarHeader
|
||||
isPinned={isSideBarPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
@@ -174,11 +232,14 @@ const SideBar: React.FC<{
|
||||
)}
|
||||
<div
|
||||
className='drag-bar absolute right-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleHorizontalDragStart}
|
||||
></div>
|
||||
</div>
|
||||
{!isSideBarPinned && (
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/20' onClick={handleClickOverlay} />
|
||||
<div
|
||||
className='overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/20'
|
||||
onClick={handleClickOverlay}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useDragBar = (handleDragMove: (e: MouseEvent) => void) => {
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
handleDragMove(e);
|
||||
},
|
||||
[handleDragMove],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
},
|
||||
[handleMouseMove, handleMouseUp],
|
||||
);
|
||||
|
||||
return { handleMouseDown };
|
||||
};
|
||||
|
||||
export default useDragBar;
|
||||
@@ -1,8 +1,13 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { MdArrowBackIosNew } 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';
|
||||
|
||||
const DISMISS_THRESHOLD = 100;
|
||||
const VELOCITY_THRESHOLD = 0.5;
|
||||
|
||||
interface DialogProps {
|
||||
id?: string;
|
||||
@@ -28,7 +33,10 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const [translateY, setTranslateY] = useState(0);
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
@@ -43,23 +51,67 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragMove = (data: { clientY: number; deltaY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
const modal = document.querySelector('.modal-box') as HTMLElement;
|
||||
const overlay = document.querySelector('.overlay') as HTMLElement;
|
||||
if (modal && overlay) {
|
||||
modal.style.transition = '';
|
||||
overlay.style.opacity = `${1 - data.clientY / window.innerHeight}`;
|
||||
}
|
||||
setTranslateY((prev) => Math.max(prev + data.deltaY, 0));
|
||||
};
|
||||
|
||||
const handleDragEnd = (velocity: number) => {
|
||||
const modal = document.querySelector('.modal-box') as HTMLElement;
|
||||
if (!modal) return;
|
||||
|
||||
if (translateY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
|
||||
modal.style.transition = `transform ${0.15 / velocity}s ease-out`;
|
||||
onClose();
|
||||
setTimeout(() => setTranslateY(0), 300);
|
||||
if (appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
} else {
|
||||
modal.style.transition = `transform 0.3s ease-out`;
|
||||
setTranslateY(0);
|
||||
if (appService?.hasHaptics) {
|
||||
impactFeedback('medium');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(handleDragMove, handleDragEnd);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
id={id ?? 'dialog'}
|
||||
open={isOpen}
|
||||
className={clsx(
|
||||
'modal sm:min-w-90 z-50 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full',
|
||||
className,
|
||||
)}
|
||||
className={clsx('modal sm:min-w-90 z-50 h-full w-full sm:w-full', className)}
|
||||
>
|
||||
<div className='overlay fixed inset-0 z-10 bg-black/30 sm:bg-black/20' />
|
||||
<div
|
||||
className={clsx(
|
||||
'modal-box settings-content flex flex-col rounded-none p-0 sm:rounded-2xl',
|
||||
'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]',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)] sm:pt-0',
|
||||
boxClassName,
|
||||
)}
|
||||
style={{
|
||||
transform: `translateY(${translateY}px)`,
|
||||
}}
|
||||
>
|
||||
{window.innerWidth < 640 && (
|
||||
<div
|
||||
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
|
||||
onMouseDown={handleDragStart}
|
||||
onTouchStart={handleDragStart}
|
||||
>
|
||||
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
|
||||
</div>
|
||||
)}
|
||||
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
|
||||
{header ? (
|
||||
header
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useDrag = (
|
||||
onDragMove: (data: { clientX: number; clientY: number; deltaX: number; deltaY: number }) => void,
|
||||
onDragEnd?: (velocity: number) => void,
|
||||
) => {
|
||||
const isDragging = useRef(false);
|
||||
const startX = useRef(0);
|
||||
const startY = useRef(0);
|
||||
const lastX = useRef(0);
|
||||
const lastY = useRef(0);
|
||||
const startTime = useRef(0);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
isDragging.current = true;
|
||||
|
||||
if ('touches' in e) {
|
||||
startY.current = e.touches[0]!.clientY;
|
||||
startX.current = e.touches[0]!.clientX;
|
||||
} else {
|
||||
startY.current = e.clientY;
|
||||
startX.current = e.clientX;
|
||||
}
|
||||
startTime.current = performance.now();
|
||||
|
||||
const handleMove = (event: MouseEvent | TouchEvent) => {
|
||||
if (isDragging.current) {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
let clientX = 0;
|
||||
let clientY = 0;
|
||||
|
||||
if ('touches' in event && event.touches.length > 0) {
|
||||
const currentTouch = event.touches[0]!;
|
||||
clientX = currentTouch.clientX;
|
||||
clientY = currentTouch.clientY;
|
||||
} else {
|
||||
const evt = event as MouseEvent;
|
||||
clientX = evt.clientX;
|
||||
clientY = evt.clientY;
|
||||
}
|
||||
deltaX = clientX - lastX.current;
|
||||
deltaY = clientY - lastY.current;
|
||||
lastX.current = clientX;
|
||||
lastY.current = clientY;
|
||||
|
||||
onDragMove({ clientX, clientY, deltaX, deltaY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnd = (event: MouseEvent | TouchEvent) => {
|
||||
isDragging.current = false;
|
||||
const endTime = performance.now();
|
||||
const deltaT = endTime - startTime.current;
|
||||
|
||||
const distanceY =
|
||||
'touches' in event
|
||||
? event.changedTouches[0]!.clientY - startY.current
|
||||
: event.clientY - startY.current;
|
||||
const velocity = distanceY / deltaT;
|
||||
|
||||
if (onDragEnd) {
|
||||
onDragEnd(velocity);
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', handleMove);
|
||||
window.removeEventListener('mouseup', handleEnd);
|
||||
window.removeEventListener('touchmove', handleMove);
|
||||
window.removeEventListener('touchend', handleEnd);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMove, { passive: true });
|
||||
window.addEventListener('mouseup', handleEnd);
|
||||
window.addEventListener('touchmove', handleMove, { passive: true });
|
||||
window.addEventListener('touchend', handleEnd);
|
||||
},
|
||||
[onDragMove, onDragEnd],
|
||||
);
|
||||
|
||||
return { handleDragStart };
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export const usePullToRefresh = (ref: React.RefObject<HTMLDivElement>, onTrigger
|
||||
const initialX = startEvent.touches[0]!.clientX;
|
||||
const initialY = startEvent.touches[0]!.clientY;
|
||||
|
||||
el.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
el.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||
el.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
function handleTouchMove(moveEvent: TouchEvent) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useScreenWakeLock = (lock: boolean) => {
|
||||
console.log('Wake lock acquired');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to acquire wake lock:', err);
|
||||
console.info('Failed to acquire wake lock:', err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export abstract class BaseAppService implements AppService {
|
||||
abstract hasContextMenu: boolean;
|
||||
abstract hasRoundedWindow: boolean;
|
||||
abstract hasSafeAreaInset: boolean;
|
||||
abstract hasHaptics: boolean;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
|
||||
@@ -126,6 +126,7 @@ export class NativeAppService extends BaseAppService {
|
||||
hasContextMenu = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
|
||||
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -188,6 +188,7 @@ export class WebAppService extends BaseAppService {
|
||||
hasContextMenu = false;
|
||||
hasRoundedWindow = false;
|
||||
hasSafeAreaInset = isPWA();
|
||||
hasHaptics = false;
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
|
||||
return resolvePath(fp, base);
|
||||
|
||||
@@ -204,6 +204,7 @@ foliate-view {
|
||||
.scroll-container {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.scroll-container.hidden-scrollbar {
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface AppService {
|
||||
hasContextMenu: boolean;
|
||||
hasRoundedWindow: boolean;
|
||||
hasSafeAreaInset: boolean;
|
||||
hasHaptics: boolean;
|
||||
isMobile: boolean;
|
||||
isAppDataSandbox: boolean;
|
||||
isAndroidApp: boolean;
|
||||
|
||||
Reference in New Issue
Block a user