diff --git a/src/modules/core/AppRoute.constants.ts b/src/modules/core/AppRoute.constants.ts index d1bb15fd..c5c7f19e 100644 --- a/src/modules/core/AppRoute.constants.ts +++ b/src/modules/core/AppRoute.constants.ts @@ -170,4 +170,14 @@ export const AppRoutes = { match: '/more', path: '/more', }, -} satisfies TAppRoutes; +} as const satisfies TAppRoutes; + +type ExtractChildRouteStringPaths = T extends { childRoutes: infer U } ? ExtractStringPaths : never; + +type ExtractStringPaths = T extends { path: infer P } + ? P extends string + ? P | ExtractChildRouteStringPaths + : ExtractChildRouteStringPaths + : ExtractChildRouteStringPaths; + +export type StaticAppRoute = ExtractStringPaths<(typeof AppRoutes)[keyof typeof AppRoutes]>; diff --git a/src/modules/navigation-bar/NavigationBar.constants.ts b/src/modules/navigation-bar/NavigationBar.constants.ts new file mode 100644 index 00000000..2f7c95fa --- /dev/null +++ b/src/modules/navigation-bar/NavigationBar.constants.ts @@ -0,0 +1,104 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import CollectionsBookmarkIcon from '@mui/icons-material/CollectionsBookmark'; +import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined'; +import NewReleasesIcon from '@mui/icons-material/NewReleases'; +import NewReleasesOutlinedIcon from '@mui/icons-material/NewReleasesOutlined'; +import HistoryIcon from '@mui/icons-material/History'; +import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined'; +import ExploreIcon from '@mui/icons-material/Explore'; +import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined'; +import GetAppIcon from '@mui/icons-material/GetApp'; +import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import SettingsIcon from '@mui/icons-material/Settings'; +import InfoIcon from '@mui/icons-material/Info'; +import { NavbarItem, NavBarItemMoreGroup } from '@/modules/navigation-bar/NavigationBar.types.ts'; +import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; + +type RestrictedNavBarItem = Omit & { show: Show }; + +const NAVIGATION_BAR_BASE_ITEMS = [ + { + path: AppRoutes.library.path, + title: 'library.title', + SelectedIconComponent: CollectionsBookmarkIcon, + IconComponent: CollectionsOutlinedBookmarkIcon, + show: 'both', + moreGroup: NavBarItemMoreGroup.GENERAL, + }, + { + path: AppRoutes.updates.path, + title: 'updates.title', + SelectedIconComponent: NewReleasesIcon, + IconComponent: NewReleasesOutlinedIcon, + show: 'both', + moreGroup: NavBarItemMoreGroup.GENERAL, + }, + { + path: AppRoutes.history.path, + title: 'history.title', + SelectedIconComponent: HistoryIcon, + IconComponent: HistoryOutlinedIcon, + show: 'both', + moreGroup: NavBarItemMoreGroup.GENERAL, + }, + { + path: AppRoutes.browse.path, + title: 'global.label.browse', + SelectedIconComponent: ExploreIcon, + IconComponent: ExploreOutlinedIcon, + show: 'both', + moreGroup: NavBarItemMoreGroup.GENERAL, + }, +] as const satisfies RestrictedNavBarItem<'both'>[]; + +const NAVIGATION_BAR_DESKTOP_ITEMS = [ + { + path: AppRoutes.downloads.path, + title: 'download.title.queue', + SelectedIconComponent: GetAppIcon, + IconComponent: GetAppOutlinedIcon, + show: 'desktop', + moreGroup: NavBarItemMoreGroup.HIDDEN_ITEM, + }, + { + path: AppRoutes.settings.path, + title: 'settings.title', + SelectedIconComponent: SettingsIcon, + IconComponent: SettingsIcon, + show: 'desktop', + moreGroup: NavBarItemMoreGroup.SETTING_INFO, + }, + { + path: AppRoutes.settings.childRoutes.about.path, + title: 'settings.about.title', + SelectedIconComponent: InfoIcon, + IconComponent: InfoIcon, + show: 'desktop', + moreGroup: NavBarItemMoreGroup.SETTING_INFO, + }, +] as const satisfies RestrictedNavBarItem<'desktop'>[]; + +export const NAVIGATION_BAR_MOBILE_ITEMS = [ + { + path: AppRoutes.more.path, + title: 'global.label.more', + SelectedIconComponent: MoreHorizIcon, + IconComponent: MoreHorizIcon, + show: 'mobile', + moreGroup: NavBarItemMoreGroup.GENERAL, + }, +] as const satisfies RestrictedNavBarItem<'mobile'>[]; + +export const NAVIGATION_BAR_ITEMS = [ + ...NAVIGATION_BAR_BASE_ITEMS, + ...NAVIGATION_BAR_DESKTOP_ITEMS, + ...NAVIGATION_BAR_MOBILE_ITEMS, +] as const satisfies NavbarItem[]; diff --git a/src/modules/navigation-bar/NavigationBar.types.ts b/src/modules/navigation-bar/NavigationBar.types.ts index 74559172..8fb321b2 100644 --- a/src/modules/navigation-bar/NavigationBar.types.ts +++ b/src/modules/navigation-bar/NavigationBar.types.ts @@ -9,18 +9,26 @@ import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon'; import { TranslationKey } from '@/Base.types.ts'; +import { StaticAppRoute } from '@/modules/core/AppRoute.constants.ts'; export interface INavbarOverride { status: boolean; value: any; } +export enum NavBarItemMoreGroup { + GENERAL, + HIDDEN_ITEM, + SETTING_INFO, +} + export interface NavbarItem { - path: string; + path: StaticAppRoute; title: TranslationKey; SelectedIconComponent: OverridableComponent>; IconComponent: OverridableComponent>; show: 'mobile' | 'desktop' | 'both'; + moreGroup: NavBarItemMoreGroup; } export type NavbarContextType = { diff --git a/src/modules/navigation-bar/NavigationBar.util.ts b/src/modules/navigation-bar/NavigationBar.util.ts new file mode 100644 index 00000000..876db942 --- /dev/null +++ b/src/modules/navigation-bar/NavigationBar.util.ts @@ -0,0 +1,55 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { NavbarItem } from '@/modules/navigation-bar/NavigationBar.types.ts'; +import { MetadataHistorySettings } from '@/modules/history/History.types.ts'; +import { AppRoutes, StaticAppRoute } from '@/modules/core/AppRoute.constants.ts'; + +type NavBarItemDeviceFilterKey = `hide${Capitalize}`; + +type FilterSettings = Pick & + Partial> & { + hideMore?: boolean; + }; + +const ITEM_TO_VISIBLE_FILTER: Partial> = { + [AppRoutes.history.path]: 'hideHistory', + [AppRoutes.more.path]: 'hideMore', +}; + +export class NavigationBarUtil { + static isPathRestricted(path: StaticAppRoute, filter: FilterSettings): boolean { + const pathVisibleFilterKey = ITEM_TO_VISIBLE_FILTER[path]; + + if (!pathVisibleFilterKey) { + return false; + } + + return !!filter[pathVisibleFilterKey]; + } + + static filterItems( + items: NavbarItem[], + { hideBoth, hideDesktop, hideMobile, ...filter }: FilterSettings, + ): NavbarItem[] { + return items + .filter((item) => !NavigationBarUtil.isPathRestricted(item.path, filter)) + .filter((item) => { + switch (item.show) { + case 'both': + return !hideBoth; + case 'desktop': + return !hideDesktop; + case 'mobile': + return !hideMobile; + default: + return true; + } + }); + } +} diff --git a/src/modules/navigation-bar/components/DefaultNavBar.tsx b/src/modules/navigation-bar/components/DefaultNavBar.tsx index bf3cc852..3e34490f 100644 --- a/src/modules/navigation-bar/components/DefaultNavBar.tsx +++ b/src/modules/navigation-bar/components/DefaultNavBar.tsx @@ -11,78 +11,22 @@ import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; -import CollectionsBookmarkIcon from '@mui/icons-material/CollectionsBookmark'; -import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined'; -import NewReleasesIcon from '@mui/icons-material/NewReleases'; -import NewReleasesOutlinedIcon from '@mui/icons-material/NewReleasesOutlined'; -import HistoryIcon from '@mui/icons-material/History'; -import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined'; -import ExploreIcon from '@mui/icons-material/Explore'; -import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined'; -import GetAppIcon from '@mui/icons-material/GetApp'; -import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; import ArrowBack from '@mui/icons-material/ArrowBack'; import { useLocation } from 'react-router-dom'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import MenuIcon from '@mui/icons-material/Menu'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; -import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; import { useGetOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { DesktopSideBar } from '@/modules/navigation-bar/components/DesktopSideBar.tsx'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { MobileBottomBar } from '@/modules/navigation-bar/components/MobileBottomBar.tsx'; -import { NavbarItem } from '@/modules/navigation-bar/NavigationBar.types.ts'; -import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; - -const navbarItems: Array = [ - { - path: AppRoutes.library.path, - title: 'library.title', - SelectedIconComponent: CollectionsBookmarkIcon, - IconComponent: CollectionsOutlinedBookmarkIcon, - show: 'both', - }, - { - path: AppRoutes.updates.path, - title: 'updates.title', - SelectedIconComponent: NewReleasesIcon, - IconComponent: NewReleasesOutlinedIcon, - show: 'both', - }, - { - path: AppRoutes.history.path, - title: 'history.title', - SelectedIconComponent: HistoryIcon, - IconComponent: HistoryOutlinedIcon, - show: 'both', - }, - { - path: AppRoutes.browse.path, - title: 'global.label.browse', - SelectedIconComponent: ExploreIcon, - IconComponent: ExploreOutlinedIcon, - show: 'both', - }, - { - path: AppRoutes.downloads.path, - title: 'download.title.queue', - SelectedIconComponent: GetAppIcon, - IconComponent: GetAppOutlinedIcon, - show: 'desktop', - }, - { - path: AppRoutes.more.path, - title: 'global.label.more', - SelectedIconComponent: MoreHorizIcon, - IconComponent: MoreHorizIcon, - show: 'both', - }, -]; +import { NAVIGATION_BAR_ITEMS } from '@/modules/navigation-bar/NavigationBar.constants.ts'; +import { NavigationBarUtil } from '@/modules/navigation-bar/NavigationBar.util.ts'; export function DefaultNavBar() { const { title, action, override, isCollapsed, setIsCollapsed, setAppBarHeight, navBarWidth, setNavBarWidth } = @@ -113,7 +57,7 @@ export function DefaultNavBar() { return () => setAppBarHeight(0); }, [override.status]); - const isMainRoute = navbarItems.some(({ path, show }) => { + const isMainRoute = NAVIGATION_BAR_ITEMS.some(({ path, show }) => { if (isMobileWidth && show === 'desktop') { return false; } @@ -126,16 +70,14 @@ export function DefaultNavBar() { }); const actualNavBarWidth = isMobileWidth || isCollapsed ? 0 : navBarWidth; - const activeNavBar: NavbarItem['show'] = isMobileWidth ? 'mobile' : 'desktop'; const visibleNavBarItems = useMemo( () => - navbarItems - .filter((item) => { - const isHistory = item.title === 'history.title'; - - return !isHistory || !hideHistory; - }) - .filter(({ show }) => ['both', activeNavBar].includes(show)), + NavigationBarUtil.filterItems(NAVIGATION_BAR_ITEMS, { + hideHistory, + hideBoth: false, + hideDesktop: isMobileWidth, + hideMobile: !isMobileWidth, + }), [isMobileWidth, hideHistory], ); const NavBarComponent = useMemo(() => (isMobileWidth ? MobileBottomBar : DesktopSideBar), [isMobileWidth]); diff --git a/src/modules/navigation-bar/components/MobileBottomBar.tsx b/src/modules/navigation-bar/components/MobileBottomBar.tsx index 696f7c71..24b96972 100644 --- a/src/modules/navigation-bar/components/MobileBottomBar.tsx +++ b/src/modules/navigation-bar/components/MobileBottomBar.tsx @@ -16,6 +16,7 @@ import { CSSObject, useTheme } from '@mui/material/styles'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { NavbarItem } from '@/modules/navigation-bar/NavigationBar.types.ts'; +import { StaticAppRoute } from '@/modules/core/AppRoute.constants.ts'; export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) => { const { t } = useTranslation(); @@ -58,7 +59,7 @@ export const MobileBottomBar = ({ navBarItems }: { navBarItems: NavbarItem[] }) { + onChange={(_, newValue: StaticAppRoute) => { setSelectedNavBarItem(newValue); navigate(newValue); }} diff --git a/src/modules/settings/screens/More.tsx b/src/modules/settings/screens/More.tsx index 13cc9026..36f37dbc 100644 --- a/src/modules/settings/screens/More.tsx +++ b/src/modules/settings/screens/More.tsx @@ -6,23 +6,57 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useLayoutEffect } from 'react'; +import { Fragment, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; import List from '@mui/material/List'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import InfoIcon from '@mui/icons-material/Info'; import ListAltIcon from '@mui/icons-material/ListAlt'; -import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; import Divider from '@mui/material/Divider'; -import SettingsIcon from '@mui/icons-material/Settings'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { ListItemLink } from '@/modules/core/components/ListItemLink.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { NAVIGATION_BAR_ITEMS } from '@/modules/navigation-bar/NavigationBar.constants.ts'; +import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; +import { NavigationBarUtil } from '@/modules/navigation-bar/NavigationBar.util.ts'; +import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { NavbarItem, NavBarItemMoreGroup } from '@/modules/navigation-bar/NavigationBar.types.ts'; export const More = () => { const { t } = useTranslation(); const { setTitle, setAction } = useNavBarContext(); + const isMobileWidth = MediaQuery.useIsMobileWidth(); + + const { + settings: { hideHistory }, + } = useMetadataServerSettings(); + + const hiddenNavBarItems = NavigationBarUtil.filterItems(NAVIGATION_BAR_ITEMS, { + hideHistory, + hideMore: true, + hideBoth: true, + hideDesktop: !isMobileWidth, + hideMobile: isMobileWidth, + }); + + const hiddenNavBarItemsByMoreGroup = Object.groupBy(hiddenNavBarItems, (item) => item.moreGroup); + + const hiddenItemsMoreGroup = [ + ...(hiddenNavBarItemsByMoreGroup[NavBarItemMoreGroup.HIDDEN_ITEM] ?? []), + { + path: AppRoutes.settings.childRoutes.categories.path, + title: 'category.title.category_other', + SelectedIconComponent: ListAltIcon, + IconComponent: ListAltIcon, + show: 'both', + moreGroup: NavBarItemMoreGroup.HIDDEN_ITEM, + }, + ] satisfies NavbarItem[]; + + const finalHiddenNavBarItemsByGroup: typeof hiddenNavBarItemsByMoreGroup = { + ...hiddenNavBarItemsByMoreGroup, + [NavBarItemMoreGroup.HIDDEN_ITEM]: hiddenItemsMoreGroup, + }; useLayoutEffect(() => { setTitle(t('global.label.more')); @@ -36,31 +70,19 @@ export const More = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - + {Object.entries(finalHiddenNavBarItemsByGroup).map(([group, items], index, list) => ( + + {items.map((item) => ( + + + + + + + ))} + {index !== list.length - 1 && } + + ))} ); };