fix: repair Android startup and improve mobile web layouts
This commit is contained in:
1 parent
4c2286e70d
commit
b0cbd0f149
30 files changed
+609
-140
No files matched your search
@@ -1,12 +1,13 @@
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, Outlet, useNavigate } from '@tanstack/react-router';
|
||||
import { Suspense, useState, type ReactNode } from 'react';
|
||||
import { Suspense, useCallback, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuthState, useSession, useUser } from '../app/session';
|
||||
import { useCollectionCounts } from '../entities/collection';
|
||||
import {
|
||||
moreIcon,
|
||||
auditFillIcon,
|
||||
auditIcon,
|
||||
groupFillIcon,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
chevronRightIcon,
|
||||
storageFillIcon,
|
||||
storageIcon, inboxFillIcon, inboxIcon, myUploadsFillIcon, myUploadsIcon, uploadFillIcon, uploadIcon, tuneFillIcon, tuneIcon} from '../shared/ui/icons';
|
||||
import { Avatar, cx, Icon, Logo, Menu } from '../shared/ui/ui';
|
||||
import { Avatar, cx, Dialog, Icon, Logo, Menu } from '../shared/ui/ui';
|
||||
import { LanguageSwitch } from './LanguageSwitch';
|
||||
import { PlayerProvider, usePlayer, usePlayerState } from '../player/context';
|
||||
import { Ambient } from './Ambient';
|
||||
@@ -65,7 +66,7 @@ function NavLink({
|
||||
badge?: number; // яркий бейдж (ждут решения)
|
||||
}) {
|
||||
return (
|
||||
<Link to={to} className={s.navItem} activeOptions={{ exact: to === '/' }}>
|
||||
<Link to={to} aria-label={label} title={label} className={s.navItem} activeOptions={{ exact: to === '/' }}>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<Icon src={isActive ? activeIcon : icon} className={s.navIcon} />
|
||||
@@ -97,12 +98,14 @@ function NavSection({
|
||||
badge,
|
||||
children,
|
||||
defaultFolded = false,
|
||||
mobile = false,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
badge?: number;
|
||||
children: ReactNode;
|
||||
defaultFolded?: boolean;
|
||||
mobile?: boolean;
|
||||
}) {
|
||||
const [folded, setFolded] = useState(() => {
|
||||
if (readIds(FOLDED_KEY).includes(id)) return true;
|
||||
@@ -123,12 +126,16 @@ function NavSection({
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={s.navSection} aria-expanded={!folded} onClick={toggle}>
|
||||
<span className={s.navSectionTitle}>{title}</span>
|
||||
{folded && badge ? <span className={s.navBadge}>{badge > 99 ? '99+' : badge}</span> : null}
|
||||
<Icon src={chevronRightIcon} size={16} className={cx(s.navChevron, !folded && s.navChevronOpen)} />
|
||||
</button>
|
||||
<div className={cx(s.navGroup, folded && s.navGroupFolded)}>{children}</div>
|
||||
{mobile ? (
|
||||
<h3 className={s.navSection}><span className={s.navSectionTitle}>{title}</span></h3>
|
||||
) : (
|
||||
<button type="button" className={s.navSection} aria-expanded={!folded} onClick={toggle}>
|
||||
<span className={s.navSectionTitle}>{title}</span>
|
||||
{folded && badge ? <span className={s.navBadge}>{badge > 99 ? '99+' : badge}</span> : null}
|
||||
<Icon src={chevronRightIcon} size={16} className={cx(s.navChevron, !folded && s.navChevronOpen)} />
|
||||
</button>
|
||||
)}
|
||||
<div className={cx(s.navGroup, !mobile && folded && s.navGroupFolded)}>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -156,6 +163,60 @@ function Shell() {
|
||||
const reviews = user.reviews_uploads;
|
||||
const pending = usePendingUploads(reviews);
|
||||
const unseen = useUnseenUploads(staff);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const closeMobile = useCallback(() => setMobileOpen(false), []);
|
||||
const navigation = (mobile = false) => (
|
||||
<>
|
||||
<NavLink to="/" label={t('nav.home')} icon={homeIcon} activeIcon={homeFillIcon} />
|
||||
<NavSection mobile={mobile} id="collection" title={t('nav.collection')}>
|
||||
<NavLink to="/collection/history" label={t('nav.history')} icon={historyIcon} activeIcon={historyFillIcon} />
|
||||
<NavLink to="/collection/likes" label={t('nav.likes')} icon={likeIcon} activeIcon={likeFillIcon} count={counts.likes} />
|
||||
<NavLink
|
||||
to="/collection/dislikes"
|
||||
label={t('nav.dislikes')}
|
||||
icon={dislikeIcon}
|
||||
activeIcon={dislikeFillIcon}
|
||||
count={counts.dislikes}
|
||||
/>
|
||||
</NavSection>
|
||||
{!staff && reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
{staff && (
|
||||
<NavSection mobile={mobile} id="admin" title={t('nav.admin')} badge={reviews ? pending : undefined} defaultFolded>
|
||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||
)}
|
||||
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
||||
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
||||
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
||||
{reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
{user.role === 'admin' && (
|
||||
<>
|
||||
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||
</>
|
||||
)}
|
||||
</NavSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div className={cx(s.shell, island && s.island)}>
|
||||
@@ -195,55 +256,26 @@ function Shell() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className={s.side} aria-label={t('nav.main')}>
|
||||
<nav className={s.side} aria-label={t('nav.main')}>{navigation()}</nav>
|
||||
<nav className={s.mobileBottom} aria-label={t('nav.main')}>
|
||||
<NavLink to="/" label={t('nav.home')} icon={homeIcon} activeIcon={homeFillIcon} />
|
||||
<NavSection id="collection" title={t('nav.collection')}>
|
||||
<NavLink to="/collection/history" label={t('nav.history')} icon={historyIcon} activeIcon={historyFillIcon} />
|
||||
<NavLink to="/collection/likes" label={t('nav.likes')} icon={likeIcon} activeIcon={likeFillIcon} count={counts.likes} />
|
||||
<NavLink
|
||||
to="/collection/dislikes"
|
||||
label={t('nav.dislikes')}
|
||||
icon={dislikeIcon}
|
||||
activeIcon={dislikeFillIcon}
|
||||
count={counts.dislikes}
|
||||
/>
|
||||
</NavSection>
|
||||
{!staff && reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
{staff && (
|
||||
<NavSection id="admin" title={t('nav.admin')} badge={reviews ? pending : undefined} defaultFolded>
|
||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||
{user.role === 'admin' && (
|
||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||
)}
|
||||
<NavLink to="/admin/library" label={t('nav.library')} icon={libraryIcon} activeIcon={libraryFillIcon} />
|
||||
<NavLink to="/uploads/new" label={t('nav.suggest')} icon={uploadIcon} activeIcon={uploadFillIcon} />
|
||||
<NavLink to="/uploads/mine" label={t('nav.my_uploads')} icon={myUploadsIcon} activeIcon={myUploadsFillIcon} badge={unseen} />
|
||||
{reviews && (
|
||||
<NavLink
|
||||
to="/admin/uploads"
|
||||
label={t('nav.upload_queue')}
|
||||
icon={inboxIcon}
|
||||
activeIcon={inboxFillIcon}
|
||||
badge={pending}
|
||||
/>
|
||||
)}
|
||||
{user.role === 'admin' && (
|
||||
<>
|
||||
<NavLink to="/admin/settings" label={t('nav.admin_settings')} icon={tuneIcon} activeIcon={tuneFillIcon} />
|
||||
<NavLink to="/admin/audit" label={t('nav.audit')} icon={auditIcon} activeIcon={auditFillIcon} />
|
||||
</>
|
||||
)}
|
||||
</NavSection>
|
||||
)}
|
||||
<NavLink to="/collection/history" label={t('nav.history')} icon={historyIcon} activeIcon={historyFillIcon} />
|
||||
<NavLink to="/collection/likes" label={t('nav.likes')} icon={likeIcon} activeIcon={likeFillIcon} />
|
||||
<button type="button" className={s.navItem} aria-label={t('nav.more')}
|
||||
aria-haspopup="dialog" aria-expanded={mobileOpen} onClick={() => setMobileOpen(true)}>
|
||||
<Icon src={moreIcon} className={s.navIcon} />
|
||||
<span className={s.navLabel}>{t('nav.more')}</span>
|
||||
{(pending ?? 0) + (unseen ?? 0) > 0 && <span className={s.mobileDot} />}
|
||||
</button>
|
||||
</nav>
|
||||
{mobileOpen && (
|
||||
<Dialog title={t('nav.main')} onClose={closeMobile}>
|
||||
<nav className={s.mobileMenu} aria-label={t('nav.main')}
|
||||
onClick={(e) => { if ((e.target as HTMLElement).closest('a')) closeMobile(); }}>
|
||||
{navigation(true)}
|
||||
</nav>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<main className={s.content}>
|
||||
<Suspense fallback={null}>
|
||||
|
||||
Reference in new issue
Block a user