fix: padding drag handler on Android, closes #1036 (#1039)

* refactor: load default icon when avatar image fails to load

* fix: padding drag handler on Android, closes #1036
This commit is contained in:
Huang Xin
2025-05-05 23:42:57 +08:00
committed by GitHub
parent 27df89e261
commit 4094fd86ff
6 changed files with 98 additions and 37 deletions
@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { PiUserCircle } from 'react-icons/pi';
import { PiUserCircleCheck } from 'react-icons/pi';
@@ -17,6 +16,7 @@ import { getStoragePlanData } from '@/utils/access';
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
import { QuotaType } from '@/types/user';
import UserAvatar from '@/components/UserAvatar';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
@@ -146,14 +146,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
labelClass='!max-w-40'
Icon={
avatarUrl ? (
<Image
src={avatarUrl}
alt={_('User avatar')}
className='rounded-full'
referrerPolicy='no-referrer'
width={iconSize}
height={iconSize}
/>
<UserAvatar url={avatarUrl} size={iconSize} DefaultIcon={PiUserCircleCheck} />
) : (
PiUserCircleCheck
)
+7 -9
View File
@@ -2,7 +2,6 @@
import clsx from 'clsx';
import React, { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { IoArrowBack } from 'react-icons/io5';
import { PiUserCircle } from 'react-icons/pi';
@@ -20,6 +19,7 @@ import { eventDispatcher } from '@/utils/event';
import { Toast } from '@/components/Toast';
import WindowButtons from '@/components/WindowButtons';
import Quota from '@/components/Quota';
import UserAvatar from '@/components/UserAvatar';
const ProfilePage = () => {
const _ = useTranslation();
@@ -191,14 +191,12 @@ const ProfilePage = () => {
<div className='mb-8 flex flex-col items-center gap-x-6 gap-y-4 md:flex-row md:items-start'>
<div className='flex-shrink-0'>
{avatarUrl ? (
<Image
src={avatarUrl}
alt={_('User avatar')}
className='border-base-100 h-16 w-16 rounded-full border-4 md:h-24 md:w-24'
referrerPolicy='no-referrer'
width={128}
height={128}
priority
<UserAvatar
url={avatarUrl}
size={128}
DefaultIcon={PiUserCircle}
className='h-16 w-16 md:h-24 md:w-24'
borderClassName='border-base-100 border-4'
/>
) : (
<PiUserCircle className='h-16 w-16 md:h-24 md:w-24' />
+26 -17
View File
@@ -3,6 +3,7 @@ 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';
@@ -40,6 +41,7 @@ const Dialog: React.FC<DialogProps> = ({
onClose,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [isFullHeightInMobile, setIsFullHeightInMobile] = React.useState(!snapHeight);
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
@@ -105,41 +107,39 @@ const Dialog: React.FC<DialogProps> = ({
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(() => {
onClose();
modal.style.transform = 'translateY(0%)';
}, 300);
if (appService?.hasHaptics) {
impactFeedback('medium');
}
} 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 = `translateY(${(1 - snapHeight) * window.innerHeight}px)`;
setTimeout(() => {
modal.style.height = `${snapHeight * 100}%`;
}, 100);
if (appService?.hasHaptics) {
impactFeedback('medium');
}
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');
}
}
if (appService?.hasHaptics) {
impactFeedback('medium');
}
};
@@ -161,7 +161,7 @@ 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',
'modal-box settings-content absolute 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',
window.innerWidth < window.innerHeight
? 'sm:h-[50%] sm:w-3/4'
@@ -175,14 +175,23 @@ const Dialog: React.FC<DialogProps> = ({
snapHeight
? {
height: `${snapHeight * 100}%`,
transform: `translateY(${(1 - snapHeight) * window.innerHeight}px)`,
bottom: 0,
}
: {}
}
>
{window.innerWidth < 640 && (
<div
className='drag-handle flex h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center'
className={clsx(
'drag-handle flex h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center',
'transition-padding-top duration-300 ease-out',
)}
style={{
paddingTop:
appService?.isAndroidApp && systemUIVisible && isFullHeightInMobile
? statusBarHeight
: 0,
}}
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
>
@@ -0,0 +1,50 @@
import clsx from 'clsx';
import Image from 'next/image';
import { IconType } from 'react-icons';
interface UserAvatarProps {
url: string;
size: number;
DefaultIcon: IconType;
className?: string;
borderClassName?: string;
}
const UserAvatar: React.FC<UserAvatarProps> = ({
url,
size,
className,
borderClassName,
DefaultIcon,
}) => {
return (
<div
className='relative flex h-full w-full items-center justify-center rounded-full'
style={{ width: size, height: size }}
>
{url ? (
<div>
<Image
src={url}
alt='User Avatar'
className={clsx('rounded-full', className, borderClassName)}
referrerPolicy='no-referrer'
width={size}
height={size}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
}}
/>
<div className='invisible absolute inset-0 flex items-center justify-center'>
<DefaultIcon className={clsx('text-neutral-content', className)} />
</div>
</div>
) : (
<DefaultIcon className='text-neutral-content' size={size} />
)}
</div>
);
};
export default UserAvatar;
-1
View File
@@ -20,7 +20,6 @@ export const useDrag = (
const handleDragStart = useCallback(
(e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
isDragging.current = true;
if ('touches' in e) {
+13 -1
View File
@@ -16,11 +16,23 @@ export const useTheme = ({
}: UseThemeProps = {}) => {
const { appService } = useEnv();
const { settings } = useSettingsStore();
const { themeColor, isDarkMode, updateAppTheme, setStatusBarHeight } = useThemeStore();
const {
themeColor,
isDarkMode,
showSystemUI,
dismissSystemUI,
updateAppTheme,
setStatusBarHeight,
} = useThemeStore();
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isMobile) {
if (systemUIVisible) {
showSystemUI();
} else {
dismissSystemUI();
}
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
}
if (appService?.isAndroidApp) {