diff --git a/packages/api/src/functions/covers/handler.ts b/packages/api/src/functions/covers/handler.ts index c0485182..30034afe 100644 --- a/packages/api/src/functions/covers/handler.ts +++ b/packages/api/src/functions/covers/handler.ts @@ -11,8 +11,7 @@ const s3 = new S3Client({ }) const lambda: ValidatedEventAPIGatewayProxyEvent = async (event) => { - const coverId = event.pathParameters?.id ?? `` - const objectKey = `cover-${coverId}` + const objectKey = event.pathParameters?.id ?? `` const format = event.queryStringParameters?.format || "image/webp" const userCover = await getCover(s3, objectKey) diff --git a/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/refreshMetadata.ts b/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/refreshMetadata.ts index 918cc0d9..2a2f0742 100644 --- a/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/refreshMetadata.ts +++ b/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/refreshMetadata.ts @@ -4,11 +4,12 @@ import { directives } from "@oboku/shared" import { fetchMetadata } from "./fetchMetadata" -import { atomicUpdate } from "@libs/couch/dbHelpers" +import { atomicUpdate, findOne } from "@libs/couch/dbHelpers" import nano from "nano" import { Logger } from "@libs/logger" import { pluginFacade } from "@libs/plugins/facade" import { computeMetadata } from "@libs/collections/computeMetadata" +import { saveOrUpdateCover } from "./saveOrUpdateCover" export const refreshMetadata = async ( collection: CollectionDocType, @@ -108,39 +109,55 @@ export const refreshMetadata = async ( ) const title = directives.removeDirectiveFromString( - linkMetadataInfo?.name ?? userTitle ?? "" + directivesFromLink.metadataTitle ?? + linkMetadataInfo?.name ?? + userTitle ?? + "" ) + const year = directivesFromLink.year ?? userStartYear - const updatedMetadataList = await fetchMetadata( + const externalMetadatas = await fetchMetadata( { title, year: year ? String(year) : undefined }, { withGoogle: true, googleApiKey, comicVineApiKey } ) - await atomicUpdate(db, "obokucollection", collection._id, (old) => { - const persistentMetadataList = - old.metadata?.filter((entry) => - (["user"] as CollectionMetadata["type"][]).includes(entry.type) - ) ?? [] - - const linkMetadata: CollectionMetadata = { - type: "link", - ...old.metadata?.find((item) => item.type === "link"), - title: linkMetadataInfo?.name - } + const linkMetadata: CollectionMetadata = { + type: "link", + ...collection.metadata?.find((item) => item.type === "link"), + title: linkMetadataInfo?.name + } - return { - ...old, - lastMetadataUpdatedAt: new Date().toISOString(), - metadataUpdateStatus: "idle", - lastMetadataUpdateError: null, - metadata: [ - ...persistentMetadataList, - ...updatedMetadataList, - linkMetadata - ] - } satisfies CollectionDocType + // try to get latest collection to stay as fresh as possible + const currentCollection = await findOne(db, "obokucollection", { + selector: { _id: collection._id } }) + + if (!currentCollection) throw new Error("Unable to find collection") + + const userMetadata = + currentCollection.metadata?.filter((entry) => entry.type === "user") ?? [] + const metadata = [...userMetadata, ...externalMetadatas, linkMetadata] + + // cannot be done in // since metadata status will trigger cover refresh + await saveOrUpdateCover(currentCollection, { + _id: currentCollection._id, + metadata + }) + + await atomicUpdate( + db, + "obokucollection", + collection._id, + (old) => + ({ + ...old, + lastMetadataUpdatedAt: new Date().toISOString(), + metadataUpdateStatus: "idle", + lastMetadataUpdateError: null, + metadata + }) satisfies CollectionDocType + ) } catch (error) { await atomicUpdate( db, diff --git a/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/saveOrUpdateCover.ts b/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/saveOrUpdateCover.ts new file mode 100644 index 00000000..b2b88d0e --- /dev/null +++ b/packages/api/src/functions/refreshMetadataCollectionLongProcess/src/saveOrUpdateCover.ts @@ -0,0 +1,37 @@ +import { isCoverExist } from "@libs/books/covers/isCoverExist" +import { saveCoverFromExternalLinkToBucket } from "@libs/books/covers/saveCoverFromExternalLinkToBucket" +import { CollectionDocType, getCollectionCoverKey } from "@oboku/shared" + +export const saveOrUpdateCover = async ( + prevCollection: Pick, + currentCollection: Pick +) => { + const existingCover = prevCollection.metadata?.find( + (metadata) => metadata.cover + )?.cover + const cover = currentCollection.metadata?.find( + (metadata) => metadata.cover + )?.cover + + if (!cover) return + + const coverKey = getCollectionCoverKey(currentCollection._id) + + if ( + existingCover && + cover.uri === existingCover.uri && + (await isCoverExist(coverKey)) + ) { + console.log(`Already have cover ${coverKey} for ${cover.uri}`) + + return + } + + try { + await saveCoverFromExternalLinkToBucket(coverKey, cover.uri) + + console.log(`Successfully saved cover ${cover.uri} at ${coverKey}`) + } catch (e) { + console.error(e) + } +} diff --git a/packages/api/src/functions/refreshMetadataLongProcess/src/updateCover.ts b/packages/api/src/functions/refreshMetadataLongProcess/src/updateCover.ts index fdebadf3..c74decfd 100644 --- a/packages/api/src/functions/refreshMetadataLongProcess/src/updateCover.ts +++ b/packages/api/src/functions/refreshMetadataLongProcess/src/updateCover.ts @@ -4,7 +4,7 @@ import { Extractor } from "node-unrar-js" import { saveCoverFromRarArchiveToBucket } from "@libs/books/covers/saveCoverFromRarArchiveToBucket" import { Context } from "./types" import { saveCoverFromExternalLinkToBucket } from "@libs/books/covers/saveCoverFromExternalLinkToBucket" -import { isBookCoverExist } from "@libs/books/covers/isBookCoverExist" +import { isCoverExist } from "@libs/books/covers/isCoverExist" export const updateCover = async ({ metadataList, @@ -29,7 +29,7 @@ export const updateCover = async ({ metadataForCover?.type === currentMetadaForCover?.type && metadataForCover?.coverLink && metadataForCover.coverLink === currentMetadaForCover?.coverLink && - (await isBookCoverExist(coverObjectKey)) + (await isCoverExist(coverObjectKey)) ) { console.log( `Skipping cover update for ${book._id} since the current and new cover link are equals` @@ -70,9 +70,10 @@ export const updateCover = async ({ metadataForCover?.type === "googleBookApi" && metadataForCover.coverLink ) { + const objectKey = `cover-${ctx.userNameHex}-${ctx.book._id}` + await saveCoverFromExternalLinkToBucket( - ctx, - ctx.book._id, + objectKey, metadataForCover.coverLink ) diff --git a/packages/api/src/libs/books/covers/isBookCoverExist.ts b/packages/api/src/libs/books/covers/isCoverExist.ts similarity index 85% rename from packages/api/src/libs/books/covers/isBookCoverExist.ts rename to packages/api/src/libs/books/covers/isCoverExist.ts index b56b6d04..18ede377 100644 --- a/packages/api/src/libs/books/covers/isBookCoverExist.ts +++ b/packages/api/src/libs/books/covers/isCoverExist.ts @@ -2,7 +2,7 @@ import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3" const s3 = new S3Client() -export const isBookCoverExist = async (coverObjectKey: string) => { +export const isCoverExist = async (coverObjectKey: string) => { try { await s3.send( new HeadObjectCommand({ diff --git a/packages/api/src/libs/books/covers/saveCoverFromExternalLinkToBucket.ts b/packages/api/src/libs/books/covers/saveCoverFromExternalLinkToBucket.ts index 4000bb20..6d1edce8 100644 --- a/packages/api/src/libs/books/covers/saveCoverFromExternalLinkToBucket.ts +++ b/packages/api/src/libs/books/covers/saveCoverFromExternalLinkToBucket.ts @@ -4,30 +4,21 @@ import { saveCoverFromBufferToBucket } from "./saveCoverFromBufferToBucket" const logger = Logger.child({ module: "saveCoverFromExternalLinkToBucket" }) -type Context = { - userNameHex: string -} - export const saveCoverFromExternalLinkToBucket = async ( - ctx: Context, - bookId: string, + coverKey: string, coverUrl: string ) => { - const objectKey = `cover-${ctx.userNameHex}-${bookId}` - - Logger.info(`prepare to save cover ${objectKey}`) + Logger.info(`prepare to save cover ${coverKey}`) try { - // @todo request is deprecated, switch to something else - // @see https://github.com/request/request/issues/3143 const response = await axios.get(coverUrl, { responseType: "arraybuffer" }) const entryAsBuffer = Buffer.from(response.data) - await saveCoverFromBufferToBucket(entryAsBuffer, objectKey) + await saveCoverFromBufferToBucket(entryAsBuffer, coverKey) - Logger.info(`cover ${objectKey} has been saved/updated`) + Logger.info(`cover ${coverKey} has been saved/updated`) } catch (e) { logger.error(e) } diff --git a/packages/api/src/libs/metadata/comicvine/getSeriesMetadata.ts b/packages/api/src/libs/metadata/comicvine/getSeriesMetadata.ts index 79f4a3f1..81f3de79 100644 --- a/packages/api/src/libs/metadata/comicvine/getSeriesMetadata.ts +++ b/packages/api/src/libs/metadata/comicvine/getSeriesMetadata.ts @@ -1,4 +1,4 @@ -import { CollectionMetadata } from "@oboku/shared" + import { CollectionMetadata } from "@oboku/shared" import { Logger } from "@libs/logger" import { URL } from "url" import axios from "axios" diff --git a/packages/api/src/libs/metadata/mangadex/getSeriesMetadata.ts b/packages/api/src/libs/metadata/mangadex/getSeriesMetadata.ts index 1b660eb6..2691dee9 100644 --- a/packages/api/src/libs/metadata/mangadex/getSeriesMetadata.ts +++ b/packages/api/src/libs/metadata/mangadex/getSeriesMetadata.ts @@ -17,6 +17,9 @@ export const getSeriesMetadata = async (metadata: { const statisticsResponse = await getStatistics([result.id]) const statistics = statisticsResponse.data.statistics ?? {} const seriesStastistics = statistics[result.id] + const cover = result.relationships.find( + (relationship) => relationship.type === "cover_art" + ) if (result) { return { @@ -27,7 +30,12 @@ export const getSeriesMetadata = async (metadata: { en: result.attributes.title.en }, // x/10 - rating: seriesStastistics?.rating?.bayesian + rating: seriesStastistics?.rating?.bayesian, + ...(cover && { + cover: { + uri: `https://mangadex.org/covers/${result.id}/${cover.attributes.fileName}` + } + }) } satisfies CollectionMetadata } } catch (e) { diff --git a/packages/api/src/libs/metadata/mangadex/searchManga.ts b/packages/api/src/libs/metadata/mangadex/searchManga.ts index a2b5c0b7..1ace2ddf 100644 --- a/packages/api/src/libs/metadata/mangadex/searchManga.ts +++ b/packages/api/src/libs/metadata/mangadex/searchManga.ts @@ -1,4 +1,5 @@ import axios from "axios" +import { parse } from "url" type Response = { result: "ok" | "unknown" @@ -56,11 +57,26 @@ type Response = { availableTranslatedLanguages: string[] latestUploadedChapter: string } - relationships: Array<{ - id: "string" - type: "author" | "artist" | "cover_art" | "manga" - related?: "prequel" | "unknown" - }> + relationships: Array< + | { + id: string + type: "author" | "artist" | "manga" + related?: "prequel" | "unknown" + } + | { + id: string + type: "cover_art" + attributes: { + fileName: string + description: string + volume: string + locale: string + version: number + createdAt: string + updatedAt: string + } + } + > }> limit: number offset: number @@ -68,14 +84,24 @@ type Response = { } export const searchManga = async (title: string) => { - return axios({ + const response = await axios({ method: "get", url: "https://api.mangadex.org/manga", headers: { "Content-Type": "application/json" }, params: { - title + title, + includes: ["cover_art"], + order: { + relevance: "desc" + } } }) + + const { url, params } = response.config + + console.log(`[mangadex.searchManga] url: ${JSON.stringify({ url, params })}`) + + return response } diff --git a/packages/api/src/libs/sync/books/createOrUpdateBook.ts b/packages/api/src/libs/sync/books/createOrUpdateBook.ts index 4881c5d5..f97c6c52 100644 --- a/packages/api/src/libs/sync/books/createOrUpdateBook.ts +++ b/packages/api/src/libs/sync/books/createOrUpdateBook.ts @@ -15,7 +15,7 @@ import { addTagsToBookIfNotExist, createBook } from "@libs/couch/dbHelpers" -import { isBookCoverExist } from "@libs/books/covers/isBookCoverExist" +import { isCoverExist } from "@libs/books/covers/isCoverExist" import { Context } from "../types" type Helpers = Parameters>[1] @@ -225,7 +225,7 @@ export const createOrUpdateBook = async ({ if ( metadataAreOlderThanModifiedDate || - !(await isBookCoverExist(coverObjectKey)) + !(await isCoverExist(coverObjectKey)) ) { await helpers .refreshBookMetadata({ bookId: existingBook?._id }) diff --git a/packages/shared/src/collections/index.ts b/packages/shared/src/collections/index.ts new file mode 100644 index 00000000..00d520f6 --- /dev/null +++ b/packages/shared/src/collections/index.ts @@ -0,0 +1,3 @@ +export const getCollectionCoverKey = (collectionId: string) => { + return `collection-${collectionId}` +} diff --git a/packages/shared/src/directives.ts b/packages/shared/src/directives.ts index 985ccaef..c267055b 100644 --- a/packages/shared/src/directives.ts +++ b/packages/shared/src/directives.ts @@ -21,6 +21,7 @@ export const extractDirectivesFromName = ( year?: string ignoreMetadata?: string | undefined isWebtoon: boolean + metadataTitle?: string } => { let isNotACollection = false let tags: string[] = [] @@ -30,6 +31,7 @@ export const extractDirectivesFromName = ( let isbn = undefined let series: boolean | undefined = undefined let ignoreMetadata: string | undefined = undefined + let metadataTitle: string | undefined = undefined let isWebtoon: boolean = false const directives = resourceId @@ -50,6 +52,11 @@ export const extractDirectivesFromName = ( ignoreMetadata = value } + if (directive.startsWith("metadata-title~")) { + const value = directive.replace(/metadata-title\~/, "") + metadataTitle = value + } + if (directive === "ignore") { isIgnored = true } @@ -92,7 +99,8 @@ export const extractDirectivesFromName = ( isbn, year, ignoreMetadata, - isWebtoon + isWebtoon, + metadataTitle } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 89ddf39f..1c53a1e6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -92,3 +92,4 @@ export * from "./utils/truncate" export * from "./utils/intersection" export * from "./utils/groupBy" export * from "./utils/mergeWith" +export * from "./collections" diff --git a/packages/shared/src/metadata/index.ts b/packages/shared/src/metadata/index.ts index 1953003f..9c320abc 100644 --- a/packages/shared/src/metadata/index.ts +++ b/packages/shared/src/metadata/index.ts @@ -43,6 +43,11 @@ export type CollectionMetadata = { startYear?: number publisherName?: string rating?: number + cover?: { + uri: string + createdAt?: string + updatedAt?: string + } status?: "completed" | "ongoing" | "unknown" /** * googleBookApi: Metadata scrapped through google book api diff --git a/packages/web/src/books/Cover.tsx b/packages/web/src/books/Cover.tsx index 1a7c6b4b..3a20efb4 100644 --- a/packages/web/src/books/Cover.tsx +++ b/packages/web/src/books/Cover.tsx @@ -89,13 +89,13 @@ export const Cover: FC = memo( }) const originalSrc = book - ? `${API_URL}/covers/${auth?.nameHex}-${book._id}?${urlParams.toString()}` + ? `${API_URL}/covers/cover-${auth?.nameHex}-${book._id}?${urlParams.toString()}` : undefined urlParams.append("format", "image/jpeg") const originalJpgSrc = book - ? `${API_URL}/covers/${auth?.nameHex}-${book._id}?${urlParams.toString()}` + ? `${API_URL}/covers/cover-${auth?.nameHex}-${book._id}?${urlParams.toString()}` : undefined const coverSrc = originalSrc && !hasError ? originalSrc : placeholder diff --git a/packages/web/src/books/bookList/BookListWithControls.tsx b/packages/web/src/books/bookList/BookListWithControls.tsx index c96be59d..652c4943 100644 --- a/packages/web/src/books/bookList/BookListWithControls.tsx +++ b/packages/web/src/books/bookList/BookListWithControls.tsx @@ -2,11 +2,13 @@ import React, { ComponentProps, FC } from "react" import { ListActionsToolbar } from "../../common/lists/ListActionsToolbar" import { useBookIdsSortedBy } from "../helpers" import { BookList } from "./BookList" +import { Stack } from "@mui/material" export const BookListWithControls: FC< { data: string[] renderEmptyList?: React.ReactNode + ListActionsToolbarProps?: Partial> } & Pick< ComponentProps, "viewMode" | "onViewModeChange" | "sorting" | "onSortingChange" @@ -17,17 +19,16 @@ export const BookListWithControls: FC< sorting, viewMode, onViewModeChange, - onSortingChange + onSortingChange, + ListActionsToolbarProps }) => { const sortedData = useBookIdsSortedBy(data, sorting) return ( -
@@ -36,6 +37,7 @@ export const BookListWithControls: FC< onViewModeChange={onViewModeChange} sorting={sorting ?? "alpha"} onSortingChange={onSortingChange} + {...ListActionsToolbarProps} />
)}
-
+ ) } diff --git a/packages/web/src/collections/CollectionDetailsScreen.tsx b/packages/web/src/collections/CollectionDetailsScreen.tsx index c9dcefb2..9654952e 100644 --- a/packages/web/src/collections/CollectionDetailsScreen.tsx +++ b/packages/web/src/collections/CollectionDetailsScreen.tsx @@ -1,5 +1,5 @@ import { TopBarNavigation } from "../navigation/TopBarNavigation" -import { Box, Typography, useTheme } from "@mui/material" +import { Box, Stack, Typography, useMediaQuery, useTheme } from "@mui/material" import { useNavigate, useParams } from "react-router-dom" import EmptyLibraryAsset from "../assets/empty-library.svg" import CollectionBgSvg from "../assets/series-bg.svg" @@ -15,11 +15,10 @@ import { COLLECTION_EMPTY_ID } from "../constants.shared" import { useEffect, useMemo } from "react" import { useBooks } from "../books/states" import { useLocalSettings } from "../settings/states" -import { isDebugEnabled } from "../debug/isDebugEnabled.shared" import { Report } from "../debug/report.shared" -import { getCollectionComputedMetadata } from "./getCollectionComputedMetadata" -import { useCollectionDisplayTitle } from "./useCollectionDisplayTitle" import { useCollectionComputedMetadata } from "./useCollectionComputedMetadata" +import { useCollectionCoverUri } from "./useCollectionCoverUri" +import placeholder from "../assets/cover-placeholder.png" type ScreenParams = { id: string @@ -57,6 +56,27 @@ export const CollectionDetailsScreen = () => { () => visibleBooks?.map((item) => item._id) ?? [], [visibleBooks] ) + const { uri: coverUri, hasCover } = useCollectionCoverUri(collection) + const headerPt = [ + `calc(${theme.spacing(1)} + ${50}px)`, + `calc(${theme.spacing(1)} + ${60}px)`, + `calc(${theme.spacing(1)} + ${70}px)` + ] + const headerHeight = [ + `calc(${headerPt[0]} + 90px)`, + `calc(${headerPt[1]} + 150px)`, + `calc(${headerPt[2]} + 250px)` + ] + const coverHeight = [ + `calc(${headerHeight[0]} - ${headerPt[0]})`, + `calc(${headerHeight[1]} - ${headerPt[1]})`, + `calc(${headerHeight[2]} - ${headerPt[2]})` + ] + const coverWidth = [ + `calc(${coverHeight[0]} / 1.5)`, + `calc(${coverHeight[1]} / 1.5)`, + `calc(${coverHeight[2]} / 1.5)` + ] const { open: openActionDrawer } = useCollectionActionsDrawer( id, @@ -76,76 +96,83 @@ export const CollectionDetailsScreen = () => { return ( <> -
+ -
- + -
- - {metadata.displayTitle} - - - {`${collection?.books?.length || 0} book(s)`} - -
-
+ /> + )} + + {!!hasCover && ( + + )} + + + {metadata.displayTitle} + + + {`${collection?.books?.length || 0} book(s)`} + + + + {
} /> -
- + + ) } diff --git a/packages/web/src/collections/lists/CollectionListItemCover.tsx b/packages/web/src/collections/lists/CollectionListItemCover.tsx index 6ca04bc0..71fa0368 100644 --- a/packages/web/src/collections/lists/CollectionListItemCover.tsx +++ b/packages/web/src/collections/lists/CollectionListItemCover.tsx @@ -19,6 +19,7 @@ import { CollectionListItemBookCovers } from "./CollectionListItemBookCovers" import { getCollectionComputedMetadata } from "../getCollectionComputedMetadata" import { CollectionListItemProgress } from "./CollectionListItemProgress" import { useCollectionReadingProgress } from "../useCollectionReadingProgress" +import { useCollectionCoverUri } from "../useCollectionCoverUri" export const CollectionListItemCover = memo(({ id }: { id: string }) => { const theme = useTheme() @@ -27,6 +28,7 @@ export const CollectionListItemCover = memo(({ id }: { id: string }) => { }) const metadata = getCollectionComputedMetadata(item) const readingProgress = useCollectionReadingProgress({ id }) + const { hasCover, uri: coverUri } = useCollectionCoverUri(item) return ( { width="100%" justifyContent="center" > - + {!!coverUri ? ( + + ) : ( + + )} {id !== COLLECTION_EMPTY_ID && ( <> diff --git a/packages/web/src/collections/useCollectionCoverUri.ts b/packages/web/src/collections/useCollectionCoverUri.ts new file mode 100644 index 00000000..ffe44366 --- /dev/null +++ b/packages/web/src/collections/useCollectionCoverUri.ts @@ -0,0 +1,26 @@ +import { CollectionDocType, getCollectionCoverKey } from "@oboku/shared" +import { API_URL } from "../constants.shared" +import { DeepReadonlyObject } from "rxdb" + +export const useCollectionCoverUri = ( + collection?: DeepReadonlyObject | null +) => { + const assetHash = collection?.lastMetadataUpdatedAt?.toString() + + const urlParams = new URLSearchParams({ + ...(assetHash && { + hash: assetHash + }) + }) + + if (!collection) return { uri: undefined, hasCover: undefined } + + const hasCover = !!collection.metadata?.find((metadata) => metadata.cover) + + if (!hasCover) return { uri: undefined, hasCover: false } + + return { + uri: `${API_URL}/covers/${getCollectionCoverKey(collection?._id)}?${urlParams.toString()}`, + hasCover: true + } +} diff --git a/packages/web/src/common/lists/ListActionsToolbar.tsx b/packages/web/src/common/lists/ListActionsToolbar.tsx index de0a0e4f..1864b28a 100644 --- a/packages/web/src/common/lists/ListActionsToolbar.tsx +++ b/packages/web/src/common/lists/ListActionsToolbar.tsx @@ -1,5 +1,12 @@ -import { ComponentProps, FC, useState } from "react" -import { Toolbar, IconButton, useTheme, Button, Badge } from "@mui/material" +import { ComponentProps, FC, memo, useState } from "react" +import { + Toolbar, + ToolbarProps, + IconButton, + useTheme, + Button, + Badge +} from "@mui/material" import { AppsRounded, FormatListBulletedRounded, @@ -45,84 +52,88 @@ export const ViewModeIconButton = ({ ) } -export const ListActionsToolbar: FC<{ - viewMode?: ListActionViewMode - sorting?: ListActionSorting - onViewModeChange?: (viewMode: ListActionViewMode) => void - onSortingChange?: (sorting: ListActionSorting) => void - numberOfFiltersApplied?: number - onFilterClick?: () => void -}> = ({ - viewMode, - onViewModeChange, - onSortingChange, - sorting, - onFilterClick, - numberOfFiltersApplied = 0 -}) => { - const theme = useTheme() - const [isSortingDialogOpened, setIsSortingDialogOpened] = useState(false) +export const ListActionsToolbar = memo( + ({ + viewMode, + onViewModeChange, + onSortingChange, + sorting, + onFilterClick, + numberOfFiltersApplied = 0, + ...rest + }: { + viewMode?: ListActionViewMode + sorting?: ListActionSorting + onViewModeChange?: (viewMode: ListActionViewMode) => void + onSortingChange?: (sorting: ListActionSorting) => void + numberOfFiltersApplied?: number + onFilterClick?: () => void + } & ToolbarProps) => { + const theme = useTheme() + const [isSortingDialogOpened, setIsSortingDialogOpened] = useState(false) - return ( - <> - - {!!onFilterClick && ( - onFilterClick && onFilterClick()} - size="large" - color="primary" - > - {numberOfFiltersApplied > 0 ? ( - + return ( + <> + + {!!onFilterClick && ( + onFilterClick && onFilterClick()} + size="large" + color="primary" + > + {numberOfFiltersApplied > 0 ? ( + + + + ) : ( - - ) : ( - - )} - - )} - {!!sorting && ( -
- -
- )} - {!!viewMode && ( - - )} -
- setIsSortingDialogOpened(false)} - open={isSortingDialogOpened} - onChange={onSortingChange} - /> - - ) -} + + + )} + {!!viewMode && ( + + )} + + setIsSortingDialogOpened(false)} + open={isSortingDialogOpened} + onChange={onSortingChange} + /> + + ) + } +)