import clsx from 'clsx'; 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 { useThemeStore } from '@/store/themeStore'; import { useDeviceControlStore } from '@/store/deviceStore'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { impactFeedback } from '@tauri-apps/plugin-haptics'; import { getDirFromUILanguage } from '@/utils/rtl'; import { eventDispatcher } from '@/utils/event'; const VELOCITY_THRESHOLD = 0.5; const SNAP_THRESHOLD = 0.2; interface DialogProps { id?: string; isOpen: boolean; children: ReactNode; snapHeight?: number; header?: ReactNode; title?: string; className?: string; bgClassName?: string; boxClassName?: string; contentClassName?: string; onClose: () => void; } const Dialog: React.FC = ({ id, isOpen, children, snapHeight, header, title, className, bgClassName, boxClassName, contentClassName, onClose, }) => { const { appService } = useEnv(); const { systemUIVisible, statusBarHeight } = useThemeStore(); const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore(); const [isFullHeightInMobile, setIsFullHeightInMobile] = React.useState(!snapHeight); const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl'); const iconSize22 = useResponsiveSize(22); const isMobile = window.innerWidth < 640; const handleKeyDown = (event: KeyboardEvent | CustomEvent) => { if (event instanceof CustomEvent) { if (event.detail.keyName === 'Back') { onClose(); return true; } } else if (event.key === 'Escape') { onClose(); } return false; }; useEffect(() => { if (!isOpen) return; window.addEventListener('keydown', handleKeyDown); if (appService?.isAndroidApp) { acquireBackKeyInterception(); eventDispatcher.onSync('native-key-down', handleKeyDown); } return () => { window.removeEventListener('keydown', handleKeyDown); if (appService?.isAndroidApp) { releaseBackKeyInterception(); eventDispatcher.offSync('native-key-down', handleKeyDown); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); 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; const heightFraction = data.clientY / window.innerHeight; const newTop = Math.max(0.0, Math.min(1, heightFraction)); if (modal && overlay) { modal.style.height = '100%'; modal.style.transform = `translateY(${newTop * 100}%)`; overlay.style.opacity = `${1 - heightFraction}`; setIsFullHeightInMobile(data.clientY < 44); modal.style.transition = `padding-top 0.3s ease-out`; } }; const handleDragEnd = (data: { velocity: number; clientY: number }) => { const modal = document.querySelector('.modal-box') as HTMLElement; const overlay = document.querySelector('.overlay') as HTMLElement; if (!modal || !overlay) return; const snapUpper = snapHeight ? 1 - snapHeight - SNAP_THRESHOLD : 0.5; const snapLower = snapHeight ? 1 - snapHeight + SNAP_THRESHOLD : 0.5; if ( data.velocity > VELOCITY_THRESHOLD || (data.velocity >= 0 && data.clientY >= window.innerHeight * snapLower) ) { // dialog is dismissed const transitionDuration = 0.15 / Math.max(data.velocity, 0.5); modal.style.height = '100%'; modal.style.transition = `transform ${transitionDuration}s ease-out`; modal.style.transform = 'translateY(100%)'; overlay.style.transition = `opacity ${transitionDuration}s ease-out`; overlay.style.opacity = '0'; onClose(); setTimeout(() => { modal.style.transform = 'translateY(0%)'; }, 300); } else if ( snapHeight && data.clientY > window.innerHeight * snapUpper && data.clientY < window.innerHeight * snapLower ) { // dialog is snapped overlay.style.transition = `opacity 0.3s ease-out`; overlay.style.opacity = `${1 - snapHeight}`; modal.style.height = `${snapHeight * 100}%`; modal.style.bottom = '0'; modal.style.transition = `transform 0.3s ease-out`; modal.style.transform = ''; } else { // dialog is opened without snap setIsFullHeightInMobile(true); modal.style.height = '100%'; modal.style.transition = `transform 0.3s ease-out`; modal.style.transform = `translateY(0%)`; overlay.style.opacity = '0'; } if (appService?.hasHaptics) { impactFeedback('medium'); } }; const { handleDragStart } = useDrag(handleDragMove, handleDragEnd); return (
{window.innerWidth < 640 && (
)}
{header ? ( header ) : (
{title ?? ''}
)}
{children}
); }; export default Dialog;