Add reusable component

This commit is contained in:
grafixeyehero 2024-01-31 04:18:12 +03:00
parent c3b5d50313
commit cc87ba3859
16 changed files with 512 additions and 5 deletions

74
package-lock.json generated
View File

@ -25,6 +25,7 @@
"@react-hook/resize-observer": "1.2.6",
"@tanstack/react-query": "4.36.1",
"@tanstack/react-query-devtools": "4.36.1",
"@types/react-lazy-load-image-component": "1.6.3",
"abortcontroller-polyfill": "1.7.5",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
@ -52,7 +53,9 @@
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"react": "17.0.2",
"react-blurhash": "0.3.0",
"react-dom": "17.0.2",
"react-lazy-load-image-component": "1.6.0",
"react-router-dom": "6.21.3",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",
@ -4705,6 +4708,15 @@
"@types/react": "^17"
}
},
"node_modules/@types/react-lazy-load-image-component": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz",
"integrity": "sha512-HsIsYz7yWWTh/bftdzGnijKD26JyofLRqM/RM80sxs7Gk13G83ew8R/ra2XzXuiZfjNEjAq/Va+NBHFF9ciwxA==",
"dependencies": {
"@types/react": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
@ -12671,8 +12683,7 @@
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
@ -12686,6 +12697,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@ -16202,6 +16218,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-blurhash": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.3.0.tgz",
"integrity": "sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==",
"peerDependencies": {
"blurhash": "^2.0.3",
"react": ">=15"
}
},
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@ -16220,6 +16245,19 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-lazy-load-image-component": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz",
"integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==",
"dependencies": {
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1"
},
"peerDependencies": {
"react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x",
"react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x"
}
},
"node_modules/react-router": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz",
@ -25943,6 +25981,15 @@
"@types/react": "^17"
}
},
"@types/react-lazy-load-image-component": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.3.tgz",
"integrity": "sha512-HsIsYz7yWWTh/bftdzGnijKD26JyofLRqM/RM80sxs7Gk13G83ew8R/ra2XzXuiZfjNEjAq/Va+NBHFF9ciwxA==",
"requires": {
"@types/react": "*",
"csstype": "^3.0.2"
}
},
"@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
@ -31866,8 +31913,7 @@
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"lodash.memoize": {
"version": "4.1.2",
@ -31881,6 +31927,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@ -34295,6 +34346,12 @@
"object-assign": "^4.1.1"
}
},
"react-blurhash": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.3.0.tgz",
"integrity": "sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==",
"requires": {}
},
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@ -34310,6 +34367,15 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-lazy-load-image-component": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.0.tgz",
"integrity": "sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==",
"requires": {
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1"
}
},
"react-router": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz",

View File

@ -86,6 +86,7 @@
"@react-hook/resize-observer": "1.2.6",
"@tanstack/react-query": "4.36.1",
"@tanstack/react-query-devtools": "4.36.1",
"@types/react-lazy-load-image-component": "1.6.3",
"abortcontroller-polyfill": "1.7.5",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
@ -113,7 +114,9 @@
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"react": "17.0.2",
"react-blurhash": "0.3.0",
"react-dom": "17.0.2",
"react-lazy-load-image-component": "1.6.0",
"react-router-dom": "6.21.3",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",

View File

@ -0,0 +1,56 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
import React, { FC } from 'react';
import Icon from '@mui/material/Icon';
import imageHelper from 'utils/image';
import DefaultName from './DefaultName';
import type { ItemDto } from 'types/itemDto';
interface DefaultIconTextProps {
item: ItemDto;
defaultCardImageIcon?: string;
}
const DefaultIconText: FC<DefaultIconTextProps> = ({
item,
defaultCardImageIcon
}) => {
if (item.CollectionType) {
return (
<Icon
className='cardImageIcon'
sx={{ color: 'inherit', fontSize: '5em' }}
aria-hidden='true'
>
{imageHelper.getLibraryIcon(item.CollectionType)}
</Icon>
);
}
if (item.Type && !(item.Type === BaseItemKind.TvChannel || item.Type === BaseItemKind.Studio )) {
return (
<Icon
className='cardImageIcon'
sx={{ color: 'inherit', fontSize: '5em' }}
aria-hidden='true'
>
{imageHelper.getItemTypeIcon(item.Type)}
</Icon>
);
}
if (defaultCardImageIcon) {
return (
<Icon
className='cardImageIcon'
sx={{ color: 'inherit', fontSize: '5em' }}
aria-hidden='true'
>
{defaultCardImageIcon}
</Icon>
);
}
return <DefaultName item={item} />;
};
export default DefaultIconText;

View File

@ -0,0 +1,23 @@
import React, { FC } from 'react';
import Box from '@mui/material/Box';
import escapeHTML from 'escape-html';
import itemHelper from 'components/itemHelper';
import { isUsingLiveTvNaming } from '../cardbuilder/cardBuilderUtils';
import type { ItemDto } from 'types/itemDto';
interface DefaultNameProps {
item: ItemDto;
}
const DefaultName: FC<DefaultNameProps> = ({ item }) => {
const defaultName = isUsingLiveTvNaming(item.Type) ?
item.Name :
itemHelper.getDisplayName(item);
return (
<Box className='cardText cardDefaultText'>
{escapeHTML(defaultName)}
</Box>
);
};
export default DefaultName;

View File

@ -0,0 +1,67 @@
import React, { FC, useCallback, useState } from 'react';
import { BlurhashCanvas } from 'react-blurhash';
import { LazyLoadImage } from 'react-lazy-load-image-component';
const imageStyle: React.CSSProperties = {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
width: '100%',
height: '100%',
zIndex: 0
};
interface ImageProps {
imgUrl: string;
blurhash?: string;
containImage: boolean;
}
const Image: FC<ImageProps> = ({
imgUrl,
blurhash,
containImage
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isLoadStarted, setIsLoadStarted] = useState(false);
const handleLoad = useCallback(() => {
setIsLoaded(true);
}, []);
const handleLoadStarted = useCallback(() => {
setIsLoadStarted(true);
}, []);
return (
<div>
{!isLoaded && isLoadStarted && blurhash && (
<BlurhashCanvas
hash={blurhash}
width= {20}
height={20}
punch={1}
style={{
...imageStyle,
borderRadius: '0.2em',
pointerEvents: 'none'
}}
/>
)}
<LazyLoadImage
key={imgUrl}
src={imgUrl}
style={{
...imageStyle,
objectFit: containImage ? 'contain' : 'cover'
}}
onLoad={handleLoad}
beforeLoad={handleLoadStarted}
/>
</div>
);
};
export default Image;

View File

@ -0,0 +1,22 @@
import React, { FC } from 'react';
import IconButton from '@mui/material/IconButton';
import InfoIcon from '@mui/icons-material/Info';
import globalize from 'scripts/globalize';
interface InfoIconButtonProps {
className?: string;
}
const InfoIconButton: FC<InfoIconButtonProps> = ({ className }) => {
return (
<IconButton
className={className}
data-action='link'
title={globalize.translate('ButtonInfo')}
>
<InfoIcon />
</IconButton>
);
};
export default InfoIconButton;

View File

@ -0,0 +1,36 @@
import { BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client';
import React, { FC } from 'react';
import Image from './Image';
import DefaultIconText from './DefaultIconText';
import type { ItemDto } from 'types/itemDto';
interface MediaProps {
item: ItemDto;
imgUrl: string | undefined;
blurhash: string | undefined;
imageType?: ImageType
defaultCardImageIcon?: string
}
const Media: FC<MediaProps> = ({
item,
imgUrl,
blurhash,
imageType,
defaultCardImageIcon
}) => {
return imgUrl ? (
<Image
imgUrl={imgUrl}
blurhash={blurhash}
containImage={item.Type === BaseItemKind.TvChannel || imageType === ImageType.Logo}
/>
) : (
<DefaultIconText
item={item}
defaultCardImageIcon={defaultCardImageIcon}
/>
);
};
export default Media;

View File

@ -0,0 +1,23 @@
import React, { FC } from 'react';
import IconButton from '@mui/material/IconButton';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import globalize from 'scripts/globalize';
interface MoreVertIconButtonProps {
className?: string;
iconClassName?: string;
}
const MoreVertIconButton: FC<MoreVertIconButtonProps> = ({ className, iconClassName }) => {
return (
<IconButton
className={className}
data-action='menu'
title={globalize.translate('ButtonMore')}
>
<MoreVertIcon className={iconClassName} />
</IconButton>
);
};
export default MoreVertIconButton;

View File

@ -0,0 +1,25 @@
import React, { FC } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import globalize from 'scripts/globalize';
interface NoItemsMessageProps {
noItemsMessage?: string;
}
const NoItemsMessage: FC<NoItemsMessageProps> = ({
noItemsMessage = 'MessageNoItemsAvailable'
}) => {
return (
<Box className='noItemsMessage centerMessage'>
<Typography variant='h2'>
{globalize.translate('MessageNothingHere')}
</Typography>
<Typography paragraph variant='h2'>
{globalize.translate(noItemsMessage)}
</Typography>
</Box>
);
};
export default NoItemsMessage;

View File

@ -0,0 +1,25 @@
import React, { FC } from 'react';
import IconButton from '@mui/material/IconButton';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import globalize from 'scripts/globalize';
interface PlayArrowIconButtonProps {
className: string;
action: string;
title: string;
iconClassName?: string;
}
const PlayArrowIconButton: FC<PlayArrowIconButtonProps> = ({ className, action, title, iconClassName }) => {
return (
<IconButton
className={className}
data-action={action}
title={globalize.translate(title)}
>
<PlayArrowIcon className={iconClassName} />
</IconButton>
);
};
export default PlayArrowIconButton;

View File

@ -0,0 +1,22 @@
import React, { FC } from 'react';
import IconButton from '@mui/material/IconButton';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import globalize from 'scripts/globalize';
interface PlaylistAddIconButtonProps {
className?: string;
}
const PlaylistAddIconButton: FC<PlaylistAddIconButtonProps> = ({ className }) => {
return (
<IconButton
className={className}
data-action='addtoplaylist'
title={globalize.translate('AddToPlaylist')}
>
<PlaylistAddIcon />
</IconButton>
);
};
export default PlaylistAddIconButton;

View File

@ -0,0 +1,24 @@
import React, { FC } from 'react';
import IconButton from '@mui/material/IconButton';
interface RightIconButtonsProps {
className?: string;
id: string;
icon: string;
title: string;
}
const RightIconButtons: FC<RightIconButtonsProps> = ({ className, id, title, icon }) => {
return (
<IconButton
className={className}
data-action='custom'
data-customaction={id}
title={title}
>
{icon}
</IconButton>
);
};
export default RightIconButtons;

View File

@ -0,0 +1,48 @@
import type { CollectionType, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client';
import type { NullableBoolean, NullableNumber, NullableString } from './itemDto';
export type AttributesOpts = {
context?: CollectionType | undefined,
parentId?: NullableString,
collectionId?: NullableString,
playlistId?: NullableString,
prefix?: NullableString,
action?: NullableString,
itemServerId?: NullableString,
itemId?: NullableString,
itemTimerId?: NullableString,
itemSeriesTimerId?: NullableString,
itemChannelId?: NullableString,
itemPlaylistItemId?: NullableString,
itemType?: NullableString,
itemMediaType?: NullableString,
itemCollectionType?: NullableString,
itemIsFolder?: NullableBoolean,
itemPath?: NullableString,
itemStartDate?: NullableString,
itemEndDate?: NullableString,
itemUserData?: UserItemDataDto
};
export type DataAttributes = {
'data-playlistitemid'?: NullableString;
'data-timerid'?: NullableString;
'data-seriestimerid'?: NullableString;
'data-serverid'?: NullableString;
'data-id'?: NullableString;
'data-type'?: NullableString;
'data-collectionid'?: NullableString;
'data-playlistid'?: NullableString;
'data-mediatype'?: NullableString;
'data-channelid'?: NullableString;
'data-path'?: NullableString;
'data-collectiontype'?: NullableString;
'data-context'?: NullableString;
'data-parentid'?: NullableString;
'data-startdate'?: NullableString;
'data-enddate'?: NullableString;
'data-prefix'?: NullableString;
'data-action'?: NullableString;
'data-positionticks'?: NullableNumber;
'data-isfolder'?: NullableBoolean;
};

View File

@ -1,3 +1,4 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
import type { DeviceInfo } from '@jellyfin/sdk/lib/generated-client/models/device-info';
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info';
@ -103,7 +104,41 @@ export function getLibraryIcon(library: string | null | undefined) {
}
}
export function getItemTypeIcon(itemType: BaseItemKind | string) {
switch (itemType) {
case BaseItemKind.MusicAlbum:
return 'album';
case BaseItemKind.MusicArtist:
case BaseItemKind.Person:
return 'person';
case BaseItemKind.Audio:
return 'audiotrack';
case BaseItemKind.Movie:
return 'movie';
case BaseItemKind.Episode:
case BaseItemKind.Series:
return 'tv';
case BaseItemKind.Program:
return 'live_tv';
case BaseItemKind.Book:
return 'book';
case BaseItemKind.Folder:
return 'folder';
case BaseItemKind.BoxSet:
return 'collections';
case BaseItemKind.Playlist:
return 'view_list';
case BaseItemKind.Photo:
return 'photo';
case BaseItemKind.PhotoAlbum:
return 'photo_album';
default:
return 'folder';
}
}
export default {
getDeviceIcon,
getLibraryIcon
getLibraryIcon,
getItemTypeIcon
};

View File

@ -3,8 +3,10 @@ import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
import * as userSettings from 'scripts/settings/userSettings';
import layoutManager from 'components/layoutManager';
import { EpisodeFilter, FeatureFilters, LibraryViewSettings, ParentId, VideoBasicFilter, ViewMode } from '../types/library';
import { LibraryTab } from 'types/libraryTab';
import type { AttributesOpts, DataAttributes } from 'types/dataAttributes';
export const getVideoBasicFilter = (libraryViewSettings: LibraryViewSettings) => {
let isHd;
@ -164,3 +166,31 @@ export const getDefaultLibraryViewSettings = (viewType: LibraryTab): LibraryView
StartIndex: 0
};
};
export function getDataAttributes(
opts: AttributesOpts
): DataAttributes {
return {
'data-context': opts.context,
'data-collectionid': opts.collectionId,
'data-playlistid': opts.playlistId,
'data-parentid': opts.parentId,
'data-playlistitemid': opts.itemPlaylistItemId,
'data-action': layoutManager.tv ? opts.action : null,
'data-serverid': opts.itemServerId,
'data-id': opts.itemId,
'data-timerid': opts.itemTimerId,
'data-seriestimerid': opts.itemSeriesTimerId,
'data-channelid': opts.itemChannelId,
'data-type': opts.itemType,
'data-mediatype': opts.itemMediaType,
'data-collectiontype': opts.itemCollectionType,
'data-isfolder': opts.itemIsFolder,
'data-path': opts.itemPath,
'data-prefix': opts.prefix,
'data-positionticks': opts.itemUserData?.PlaybackPositionTicks,
'data-startdate': opts.itemStartDate?.toString(),
'data-enddate': opts.itemEndDate?.toString()
};
}

View File

@ -200,6 +200,8 @@ const config = {
path.resolve(__dirname, 'node_modules/markdown-it'),
path.resolve(__dirname, 'node_modules/mdurl'),
path.resolve(__dirname, 'node_modules/punycode'),
path.resolve(__dirname, 'node_modules/react-blurhash'),
path.resolve(__dirname, 'node_modules/react-lazy-load-image-component'),
path.resolve(__dirname, 'node_modules/react-router'),
path.resolve(__dirname, 'node_modules/screenfull'),
path.resolve(__dirname, 'node_modules/ssr-window'),