diff --git a/src/components/context/LibraryOptionsContext.tsx b/src/components/context/LibraryOptionsContext.tsx index ddbda8da..e7999971 100644 --- a/src/components/context/LibraryOptionsContext.tsx +++ b/src/components/context/LibraryOptionsContext.tsx @@ -8,6 +8,7 @@ import React, { useContext } from 'react'; import { LibraryOptions } from '@/typings'; +import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts'; type ContextType = { options: LibraryOptions; @@ -20,31 +21,8 @@ export enum GridLayout { List = 2, } -export const DefaultLibraryOptions: LibraryOptions = { - // display options - showContinueReadingButton: false, - showDownloadBadge: false, - showUnreadBadge: false, - gridLayout: GridLayout.Compact, - sourceGridLayout: GridLayout.Compact, - - showTabSize: false, - - // sort options - sortDesc: undefined, - sortBy: undefined, - - // filter options - hasDownloadedChapters: undefined, - hasBookmarkedChapters: undefined, - hasUnreadChapters: undefined, - hasDuplicateChapters: undefined, - hasTrackerBinding: {}, - hasStatus: {} as LibraryOptions['hasStatus'], -}; - export const LibraryOptionsContext = React.createContext({ - options: DefaultLibraryOptions, + options: getDefaultCategoryMetadata(), setOptions: () => {}, }); diff --git a/src/components/library/LibraryMangaGrid.tsx b/src/components/library/LibraryMangaGrid.tsx index b72bb25a..71393e4a 100644 --- a/src/components/library/LibraryMangaGrid.tsx +++ b/src/components/library/LibraryMangaGrid.tsx @@ -29,11 +29,10 @@ export const LibraryMangaGrid: React.FC = ({ const [query] = useQueryParam('query', StringParam); const { options } = useLibraryOptionsContext(); - const { unread, downloaded } = options; useEffect(() => { window.scrollTo(0, 0); - }, [query, unread, downloaded]); + }, [query, options.hasUnreadChapters, options.hasDownloadedChapters]); useLayoutEffect(() => { document.body.style.overflowY = options.gridLayout === GridLayout.List ? 'auto' : 'scroll'; diff --git a/src/components/library/LibraryOptionsPanel.tsx b/src/components/library/LibraryOptionsPanel.tsx index 64523864..289adf6b 100644 --- a/src/components/library/LibraryOptionsPanel.tsx +++ b/src/components/library/LibraryOptionsPanel.tsx @@ -8,20 +8,27 @@ import FormLabel from '@mui/material/FormLabel'; import RadioGroup from '@mui/material/RadioGroup'; -import React from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { LibraryOptions, LibrarySortMode, TranslationKey } from '@/typings'; +import { LibrarySortMode, TranslationKey } from '@/typings'; import { CheckboxInput } from '@/components/atoms/CheckboxInput'; import { RadioInput } from '@/components/atoms/RadioInput'; import { SortRadioInput } from '@/components/atoms/SortRadioInput'; import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput'; -import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; +import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { OptionsTabs } from '@/components/molecules/OptionsTabs'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { Trackers } from '@/lib/data/Trackers.ts'; import { GetTrackersSettingsQuery, MangaStatus } from '@/lib/graphql/generated/graphql.ts'; import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts'; import { statusToTranslationKey } from '@/lib/data/Mangas.ts'; +import { CategoryMetadataInfo } from '@/lib/data/Categories.ts'; +import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts'; +import { makeToast } from '@/components/util/Toast.tsx'; +import { + createUpdateMetadataServerSettings, + useMetadataServerSettings, +} from '@/lib/metadata/metadataServerSettings.ts'; const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = { filter: 'global.label.filter', @@ -39,21 +46,31 @@ const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [ ['latestUploadedChapter', 'library.option.sort.label.by_latest_uploaded_chapter'], ]; -interface IProps { +export const LibraryOptionsPanel = ({ + category, + open, + onClose, +}: { + category: CategoryMetadataInfo; open: boolean; onClose: () => void; -} - -export const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { +}) => { const { t } = useTranslation(); - const { options, setOptions } = useLibraryOptionsContext(); const trackerList = requestManager.useGetTrackerList(GET_TRACKERS_SETTINGS); const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []); - const handleFilterChange = (key: T, value: LibraryOptions[T]) => { - setOptions((v) => ({ ...v, [key]: value })); - }; + const categoryLibraryOptions = useMemo(() => getCategoryMetadata(category), [category]); + const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, () => + makeToast(t('global.error.label.failed_to_save_changes', 'error')), + ); + + const { + settings: { showTabSize }, + } = useMetadataServerSettings(); + const setSettingValue = createUpdateMetadataServerSettings<'showTabSize'>(() => + makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), + ); return ( @@ -67,32 +84,33 @@ export const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { <> handleFilterChange('hasUnreadChapters', c)} + checked={categoryLibraryOptions.hasUnreadChapters} + onChange={(c) => updateCategoryLibraryOptions('hasUnreadChapters', c)} /> handleFilterChange('hasDownloadedChapters', c)} + checked={categoryLibraryOptions.hasDownloadedChapters} + onChange={(c) => updateCategoryLibraryOptions('hasDownloadedChapters', c)} /> handleFilterChange('hasBookmarkedChapters', c)} + checked={categoryLibraryOptions.hasBookmarkedChapters} + onChange={(c) => updateCategoryLibraryOptions('hasBookmarkedChapters', c)} /> handleFilterChange('hasDuplicateChapters', c)} + checked={categoryLibraryOptions.hasDuplicateChapters} + onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)} /> {t('manga.label.status')} {Object.values(MangaStatus).map((status) => ( - handleFilterChange('hasStatus', { - ...options.hasStatus, + updateCategoryLibraryOptions('hasStatus', { + ...categoryLibraryOptions.hasStatus, [status]: checked, }) } @@ -103,10 +121,10 @@ export const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { - handleFilterChange('hasTrackerBinding', { - ...options.hasTrackerBinding, + updateCategoryLibraryOptions('hasTrackerBinding', { + ...categoryLibraryOptions.hasTrackerBinding, [tracker.id]: checked, }) } @@ -120,24 +138,24 @@ export const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { - mode !== options.sortBy - ? handleFilterChange('sortBy', mode) - : handleFilterChange('sortDesc', !options.sortDesc) + mode !== categoryLibraryOptions.sortBy + ? updateCategoryLibraryOptions('sortBy', mode) + : updateCategoryLibraryOptions('sortDesc', !categoryLibraryOptions.sortDesc) } /> )); } if (key === 'display') { - const { gridLayout, showContinueReadingButton, showDownloadBadge, showUnreadBadge, showTabSize } = - options; + const { gridLayout, showContinueReadingButton, showDownloadBadge, showUnreadBadge } = + categoryLibraryOptions; return ( <> {t('global.grid_layout.title')} handleFilterChange('gridLayout', Number(e.target.value))} + onChange={(e) => updateCategoryLibraryOptions('gridLayout', Number(e.target.value))} value={gridLayout} > = ({ open, onClose }) => { handleFilterChange('showUnreadBadge', !showUnreadBadge)} + onChange={() => updateCategoryLibraryOptions('showUnreadBadge', !showUnreadBadge)} /> handleFilterChange('showDownloadBadge', !showDownloadBadge)} + onChange={() => updateCategoryLibraryOptions('showDownloadBadge', !showDownloadBadge)} /> {t('library.option.display.tab.title')} handleFilterChange('showTabSize', !showTabSize)} + onChange={() => setSettingValue('showTabSize', !showTabSize)} /> {t('global.label.other')} @@ -181,7 +199,10 @@ export const LibraryOptionsPanel: React.FC = ({ open, onClose }) => { label={t('library.option.display.other.label.show_continue_reading_button')} checked={showContinueReadingButton} onChange={() => - handleFilterChange('showContinueReadingButton', !showContinueReadingButton) + updateCategoryLibraryOptions( + 'showContinueReadingButton', + !showContinueReadingButton, + ) } /> diff --git a/src/components/library/LibraryOptionsProvider.tsx b/src/components/library/LibraryOptionsProvider.tsx index 37dd4208..5d50702e 100644 --- a/src/components/library/LibraryOptionsProvider.tsx +++ b/src/components/library/LibraryOptionsProvider.tsx @@ -9,17 +9,18 @@ import React, { useMemo } from 'react'; import { LibraryOptions } from '@/typings'; import { useLocalStorage } from '@/util/useStorage.tsx'; -import { LibraryOptionsContext, DefaultLibraryOptions } from '@/components/context/LibraryOptionsContext'; +import { LibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; +import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts'; interface IProps { children: React.ReactNode; } export const LibraryOptionsContextProvider: React.FC = ({ children }) => { - const [options, setOptions] = useLocalStorage('libraryOptions', DefaultLibraryOptions); + const [options, setOptions] = useLocalStorage('libraryOptions', getDefaultCategoryMetadata()); const value = useMemo( - () => ({ options: { ...DefaultLibraryOptions, ...options }, setOptions }), + () => ({ options: { ...getDefaultCategoryMetadata(), ...options }, setOptions }), [options, setOptions], ); diff --git a/src/components/library/LibraryToolbarMenu.tsx b/src/components/library/LibraryToolbarMenu.tsx index 6d2db0aa..c66a7b1d 100644 --- a/src/components/library/LibraryToolbarMenu.tsx +++ b/src/components/library/LibraryToolbarMenu.tsx @@ -9,12 +9,16 @@ import FilterList from '@mui/icons-material/FilterList'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; -import React, { useState } from 'react'; +import { ComponentProps, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { LibraryOptionsPanel } from '@/components/library/LibraryOptionsPanel'; import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; -export const LibraryToolbarMenu: React.FC = () => { +export const LibraryToolbarMenu = ({ + category, +}: { + category: ComponentProps['category']; +}) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -31,7 +35,7 @@ export const LibraryToolbarMenu: React.FC = () => { - setOpen(false)} /> + setOpen(false)} /> ); }; diff --git a/src/components/library/useGetVisibleLibraryMangas.ts b/src/components/library/useGetVisibleLibraryMangas.ts index 9798f165..a415d4d0 100644 --- a/src/components/library/useGetVisibleLibraryMangas.ts +++ b/src/components/library/useGetVisibleLibraryMangas.ts @@ -9,11 +9,12 @@ import { StringParam, useQueryParam } from 'use-query-params'; import { useMemo } from 'react'; import { LibraryOptions, LibrarySortMode, NullAndUndefined } from '@/typings.ts'; -import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts'; import { MangaChapterCountInfo, MangaIdInfo } from '@/lib/data/Mangas.ts'; import { enhancedCleanup } from '@/lib/data/Strings.ts'; +import { CategoryMetadataInfo } from '@/lib/data/Categories.ts'; +import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts'; const triStateFilter = ( triState: NullAndUndefined, @@ -198,12 +199,13 @@ const sortManga = ( export const useGetVisibleLibraryMangas = ( mangas: Manga[], + category?: CategoryMetadataInfo, ): { visibleMangas: Manga[]; showFilteredOutMessage: boolean; } => { const [query] = useQueryParam('query', StringParam); - const { options } = useLibraryOptionsContext(); + const options = getCategoryMetadata(category); const { hasUnreadChapters, hasDownloadedChapters, diff --git a/src/components/manga/MangaBadges.tsx b/src/components/manga/MangaBadges.tsx index b2cff63a..3c570b82 100644 --- a/src/components/manga/MangaBadges.tsx +++ b/src/components/manga/MangaBadges.tsx @@ -10,9 +10,9 @@ import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; import { MangaCardMode } from '@/components/manga/MangaCard.types.tsx'; import { MediaQuery } from '@/lib/ui/MediaQuery.tsx'; +import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; const BadgeContainer = styled('div')(({ theme }) => ({ display: 'flex', diff --git a/src/components/source/SourceGridLayout.tsx b/src/components/source/SourceGridLayout.tsx index fce970ce..b6b78570 100644 --- a/src/components/source/SourceGridLayout.tsx +++ b/src/components/source/SourceGridLayout.tsx @@ -6,18 +6,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; +import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { GridLayouts } from '@/components/source/GridLayouts.tsx'; +import { useLocalStorage } from '@/util/useStorage.tsx'; export function SourceGridLayout() { - const { - options: { SourcegridLayout }, - setOptions, - } = useLibraryOptionsContext(); + const [sourceGridLayout, setSourceGridLayout] = useLocalStorage('source-grid-layout', GridLayout.Compact); - function setGridContextOptions(gridLayout: GridLayout) { - setOptions((prev: any) => ({ ...prev, SourcegridLayout: gridLayout })); - } - - return ; + return ; } diff --git a/src/lib/data/Categories.ts b/src/lib/data/Categories.ts index 31bddcc4..945f835e 100644 --- a/src/lib/data/Categories.ts +++ b/src/lib/data/Categories.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { CategoryType } from '@/lib/graphql/generated/graphql.ts'; +import { CategoryMetaType, CategoryType } from '@/lib/graphql/generated/graphql.ts'; export const DEFAULT_CATEGORY_ID = 0; @@ -15,6 +15,7 @@ export type CategoryNameInfo = Pick; export type CategoryDefaultInfo = Pick; export type CategoryUpdateInclusionInfo = Pick; export type CategoryDownloadInclusionInfo = Pick; +export type CategoryMetadataInfo = CategoryIdInfo & { meta: Pick[] }; export class Categories { static getIds(categories: CategoryIdInfo[]): number[] { diff --git a/src/lib/graphql/fragments/CategoryFragments.ts b/src/lib/graphql/fragments/CategoryFragments.ts index 6c845d96..701c958f 100644 --- a/src/lib/graphql/fragments/CategoryFragments.ts +++ b/src/lib/graphql/fragments/CategoryFragments.ts @@ -24,6 +24,10 @@ export const CATEGORY_LIBRARY_FIELDS = gql` fragment CATEGORY_LIBRARY_FIELDS on CategoryType { ...CATEGORY_BASE_FIELDS + meta { + key + value + } mangas { totalCount } diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index c81c09dc..24e94a11 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2713,7 +2713,7 @@ export type WebUiUpdateStatus = { export type CategoryBaseFieldsFragment = { __typename?: 'CategoryType', id: number, name: string, default: boolean, order: number }; -export type CategoryLibraryFieldsFragment = { __typename?: 'CategoryType', id: number, name: string, default: boolean, order: number, mangas: { __typename?: 'MangaNodeList', totalCount: number } }; +export type CategoryLibraryFieldsFragment = { __typename?: 'CategoryType', id: number, name: string, default: boolean, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }; export type CategorySettingFieldsFragment = { __typename?: 'CategoryType', includeInUpdate: IncludeOrExclude, includeInDownload: IncludeOrExclude, id: number, name: string, default: boolean, order: number }; @@ -3253,7 +3253,7 @@ export type GetCategoriesLibraryQueryVariables = Exact<{ }>; -export type GetCategoriesLibraryQuery = { __typename?: 'Query', categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', id: number, name: string, default: boolean, order: number, mangas: { __typename?: 'MangaNodeList', totalCount: number } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; +export type GetCategoriesLibraryQuery = { __typename?: 'Query', categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', id: number, name: string, default: boolean, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null } } }; export type GetCategoriesSettingsQueryVariables = Exact<{ after?: InputMaybe; diff --git a/src/lib/metadata/categoryMetadata.ts b/src/lib/metadata/categoryMetadata.ts new file mode 100644 index 00000000..283f6477 --- /dev/null +++ b/src/lib/metadata/categoryMetadata.ts @@ -0,0 +1,96 @@ +/* + * 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 { + AllowedMetadataValueTypes, + AppMetadataKeys, + CategoryMetadataKeys, + GqlMetaHolder, + ICategoryMetadata, + LibraryOptions, + Metadata, +} from '@/typings.ts'; +import { jsonSaveParse } from '@/util/HelperFunctions.ts'; +import { convertFromGqlMeta, getMetadataFrom, requestUpdateCategoryMetadata } from '@/lib/metadata/metadata.ts'; +import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; +import { CategoryIdInfo } from '@/lib/data/Categories.ts'; +import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; + +export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({ + // display options + showContinueReadingButton: false, + showDownloadBadge: false, + showUnreadBadge: false, + gridLayout: GridLayout.Compact, + + // sort options + sortDesc: undefined, + sortBy: undefined, + + // filter options + hasDownloadedChapters: undefined, + hasBookmarkedChapters: undefined, + hasUnreadChapters: undefined, + hasDuplicateChapters: undefined, + hasTrackerBinding: {}, + hasStatus: {} as LibraryOptions['hasStatus'], +}); + +const convertAppMetadataToGqlMetadata = ( + metadata: Partial, +): Metadata => ({ + ...metadata, + hasTrackerBinding: metadata.hasTrackerBinding ? JSON.stringify(metadata.hasTrackerBinding) : undefined, + hasStatus: metadata.hasStatus ? JSON.stringify(metadata.hasStatus) : undefined, +}); + +const convertGqlMetadataToAppMetadata = ( + metadata: Partial>, +): ICategoryMetadata => ({ + ...(metadata as unknown as ICategoryMetadata), + hasTrackerBinding: + jsonSaveParse(metadata.hasTrackerBinding as string) ?? + (undefined as any), + hasStatus: jsonSaveParse(metadata.hasStatus as string) ?? (undefined as any), +}); + +const getCategoryMetadataWithDefaultValueFallback = ( + meta?: Metadata, + defaultMetadata: ICategoryMetadata = getDefaultCategoryMetadata(), + applyMetadataMigration: boolean = true, +): ICategoryMetadata => + convertGqlMetadataToAppMetadata( + getMetadataFrom({ meta }, convertAppMetadataToGqlMetadata(defaultMetadata), applyMetadataMigration), + ); + +export const getCategoryMetadata = ( + { meta }: GqlMetaHolder = {}, + defaultMetadata?: ICategoryMetadata, + applyMetadataMigration?: boolean, +): ICategoryMetadata => + getCategoryMetadataWithDefaultValueFallback(convertFromGqlMeta(meta), defaultMetadata, applyMetadataMigration); + +export const updateCategoryMetadata = async < + MetadataKeys extends CategoryMetadataKeys = CategoryMetadataKeys, + MetadataKey extends MetadataKeys = MetadataKeys, +>( + category: CategoryIdInfo & GqlMetaHolder, + metadataKey: MetadataKey, + value: ICategoryMetadata[MetadataKey], +): Promise => + requestUpdateCategoryMetadata(category, [ + [metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]], + ]); + +export const createUpdateCategoryMetadata = + ( + category: CategoryIdInfo & GqlMetaHolder, + handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateCategoryMetadata'), + ): ((...args: OmitFirst>>) => Promise) => + (metadataKey, value) => + updateCategoryMetadata(category, metadataKey, value).catch(handleError); diff --git a/src/lib/metadata/metadata.ts b/src/lib/metadata/metadata.ts index 3ecd2f2c..6f578e82 100644 --- a/src/lib/metadata/metadata.ts +++ b/src/lib/metadata/metadata.ts @@ -34,6 +34,24 @@ const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [ 'showAddToLibraryCategorySelectDialog', 'ignoreFilters', 'removeMangaFromCategories', + 'showTabSize', + + // library category options + // filter + 'hasDownloadedChapters', + 'hasBookmarkedChapters', + 'hasUnreadChapters', + 'hasDuplicateChapters', + 'hasTrackerBinding', + 'hasStatus', + // sort + 'sortBy', + 'sortDesc', + // display + 'showDownloadBadge', + 'showUnreadBadge', + 'showTabSize', + 'showContinueReadingButton', // client 'devices', @@ -162,6 +180,10 @@ const convertValueFromMetadata = ({ showAddToLibraryCategorySelectDialog: true, ignoreFilters: false, removeMangaFromCategories: false, + showTabSize: false, // client devices: [DEFAULT_DEVICE], diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index 9c9c8342..998c8d83 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -9,7 +9,7 @@ import Chip, { ChipProps } from '@mui/material/Chip'; import Tab from '@mui/material/Tab'; import { styled } from '@mui/material/styles'; -import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useQueryParam, NumberParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; @@ -20,7 +20,6 @@ import { LibraryToolbarMenu } from '@/components/library/LibraryToolbarMenu'; import { LibraryMangaGrid } from '@/components/library/LibraryMangaGrid'; import { AppbarSearch } from '@/components/util/AppbarSearch'; import { UpdateChecker } from '@/components/library/UpdateChecker'; -import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionSelectMode } from '@/components/collection/SelectableCollectionSelectMode.tsx'; @@ -39,6 +38,9 @@ import { import { GET_CATEGORIES_LIBRARY } from '@/lib/graphql/queries/CategoryQuery.ts'; import { Mangas } from '@/lib/data/Mangas.ts'; import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts'; +import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; +import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; +import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts'; const TitleWithSizeTag = styled('span')({ display: 'flex', @@ -52,7 +54,10 @@ const TitleSizeTag = ({ sx, ...props }: ChipProps) => ( export function Library() { const { t } = useTranslation(); - const { options } = useLibraryOptionsContext(); + const { + settings: { showTabSize }, + } = useMetadataServerSettings(); + const { data: categoriesResponse, error: tabsError, @@ -75,7 +80,13 @@ export function Library() { const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam); - const activeTab = tabs.find((tab) => tab.order === tabSearchParam) ?? tabs[0]; + const { setOptions } = useLibraryOptionsContext(); + const activeTab: (typeof tabs)[number] | undefined = tabs.find((tab) => tab.order === tabSearchParam) ?? tabs[0]; + + useLayoutEffect(() => { + setOptions(getCategoryMetadata(activeTab)); + }, [activeTab]); + const { data: categoryMangaResponse, error: mangaError, @@ -83,7 +94,7 @@ export function Library() { refetch: refetchCategoryMangas, } = requestManager.useGetCategoryMangas(activeTab?.id, { skip: !activeTab, notifyOnNetworkStatusChange: true }); const categoryMangas = categoryMangaResponse?.mangas.nodes ?? []; - const { visibleMangas: mangas, showFilteredOutMessage } = useGetVisibleLibraryMangas(categoryMangas); + const { visibleMangas: mangas, showFilteredOutMessage } = useGetVisibleLibraryMangas(categoryMangas, activeTab); const retryFetchCategoryMangas = useCallback( () => refetchCategoryMangas().catch(defaultPromiseErrorHandler('Library::refetchCategoryMangas')), @@ -152,7 +163,7 @@ export function Library() { const navBarTitle = ( {title} - {options.showTabSize && } + {showTabSize && } ); setTitle(navBarTitle, title); @@ -161,7 +172,7 @@ export function Library() { {!isSelectModeActive && ( <> - + )} @@ -192,13 +203,13 @@ export function Library() { t, librarySize, areCategoriesLoading, - options, isSelectModeActive, areNoItemsSelected, areAllItemsSelected, selectedItemIds.length, mangas.length, activeTab, + showTabSize, ]); const handleTabChange = (newTab: number) => { @@ -252,7 +263,7 @@ export function Library() { label={ {tab.name} - {options.showTabSize ? : null} + {showTabSize ? : null} } value={tab.order} diff --git a/src/screens/SourceMangas.tsx b/src/screens/SourceMangas.tsx index e9904e6e..ae3133d5 100644 --- a/src/screens/SourceMangas.tsx +++ b/src/screens/SourceMangas.tsx @@ -26,7 +26,7 @@ import { AbortableApolloUseMutationPaginatedResponse, SPECIAL_ED_SOURCES, } from '@/lib/requests/RequestManager.ts'; -import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; +import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { SourceGridLayout } from '@/components/source/SourceGridLayout'; import { AppbarSearch } from '@/components/util/AppbarSearch'; import { SourceOptions } from '@/components/source/SourceOptions'; @@ -39,7 +39,7 @@ import { } from '@/lib/graphql/generated/graphql.ts'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; -import { useSessionStorage } from '@/util/useStorage.tsx'; +import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx'; import { AppStorage } from '@/util/AppStorage.ts'; import { getGridSnapshotKey } from '@/components/MangaGrid.tsx'; import { createUpdateSourceMetadata, getSourceMetadata } from '@/lib/metadata/sourceMetadata.ts'; @@ -217,7 +217,7 @@ export function SourceMangas() { settings: { hideLibraryEntries }, } = useMetadataServerSettings(); - const { options } = useLibraryOptionsContext(); + const [sourceGridLayout] = useLocalStorage('source-grid-layout', GridLayout.Compact); const [query] = useQueryParam('query', StringParam); const [currentFiltersToApply, setCurrentFiltersToApply] = useSessionStorage( `source-mangas-${sourceId}-filters`, @@ -462,7 +462,7 @@ export function SourceMangas() { message={message} messageExtra={messageExtra} isLoading={isLoading} - gridLayout={options.SourcegridLayout} + gridLayout={sourceGridLayout} mode="source" inLibraryIndicator /> diff --git a/src/typings.ts b/src/typings.ts index e9c54b9a..95a04baa 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -46,6 +46,8 @@ export interface ISourceMetadata { savedSearches?: Record; } +export interface ICategoryMetadata extends LibraryOptions {} + export type SourceFilters = GetSourceBrowseQuery['source']['filters'][number]; export interface IMetadataMigration { @@ -72,7 +74,7 @@ export type MetadataHolder = { meta?: Metadata; }; -export type AllowedMetadataValueTypes = string | boolean | number | undefined; +export type AllowedMetadataValueTypes = string | boolean | number | undefined | null; export type MetadataServerSettingKeys = keyof MetadataServerSettings; @@ -82,7 +84,14 @@ export type SearchMetadataKeys = keyof ISearchSettings; export type SourceMetadataKeys = keyof ISourceMetadata; -export type AppMetadataKeys = MetadataServerSettingKeys | MangaMetadataKeys | SearchMetadataKeys | SourceMetadataKeys; +export type CategoryMetadataKeys = keyof ICategoryMetadata; + +export type AppMetadataKeys = + | MetadataServerSettingKeys + | MangaMetadataKeys + | SearchMetadataKeys + | SourceMetadataKeys + | CategoryMetadataKeys; export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes]; @@ -131,6 +140,7 @@ export type MetadataLibrarySettings = { showAddToLibraryCategorySelectDialog: boolean; ignoreFilters: boolean; removeMangaFromCategories: boolean; + showTabSize: boolean; }; export type MetadataClientSettings = { @@ -272,8 +282,6 @@ export interface LibraryOptions { showDownloadBadge: boolean; showUnreadBadge: boolean; gridLayout: GridLayout; - sourceGridLayout: GridLayout; - showTabSize: boolean; // sort options sortBy: NullAndUndefined;