Add categories for built-in plugins

This commit is contained in:
Bill Thornton
2025-07-11 11:59:33 -04:00
parent a9106642bd
commit 0eeed43d85
5 changed files with 98 additions and 58 deletions

View File

@@ -3,12 +3,14 @@ import { useMemo } from 'react';
import { useApi } from 'hooks/useApi';
import { useConfigurationPages } from './useConfigurationPages';
import { usePlugins } from './usePlugins';
import { PluginCategory } from '../constants/pluginCategory';
import type { PluginDetails } from '../types/PluginDetails';
import { findBestConfigurationPage } from './configurationPage';
import { findBestPluginInfo } from './pluginInfo';
import { useConfigurationPages } from './useConfigurationPages';
import { usePackages } from './usePackages';
import { usePlugins } from './usePlugins';
export const usePluginDetails = () => {
const { api } = useApi();
@@ -64,9 +66,25 @@ export const usePluginDetails = () => {
imageUrl = api?.getUri(`/Plugins/${pluginInfo.Id}/${pluginInfo.Version}/Image`);
}
let category = packageInfo?.category;
if (!packageInfo) {
switch (id) {
case 'a629c0dafac54c7e931a7174223f14c8': // AudioDB
case '8c95c4d2e50c4fb0a4f36c06ff0f9a1a': // MusicBrainz
category = PluginCategory.Music;
break;
case 'a628c0dafac54c7e9d1a7134223f14c8': // OMDb
case 'b8715ed16c4745289ad3f72deb539cd4': // TMDb
category = PluginCategory.MoviesAndShows;
break;
case '872a78491171458da6fb3de3d442ad30': // Studio Images
category = PluginCategory.General;
}
}
return {
canUninstall: !!pluginInfo?.CanUninstall,
category: packageInfo?.category,
category,
description: pluginInfo?.Description || packageInfo?.description || packageInfo?.overview,
id,
imageUrl: imageUrl || packageInfo?.imageUrl || undefined,

View File

@@ -1,15 +1,14 @@
import { PluginCategory } from './pluginCategory';
/** A mapping of category names used by the plugin repository to translation keys. */
export const CATEGORY_LABELS: Record<string, string> = {
Administration: 'HeaderAdmin',
General: 'General',
Anime: 'Anime',
// Authentication: 'LabelAuthProvider', // Legacy
Books: 'Books',
// Channel: 'Channels', // Unused?
LiveTV: 'LiveTV',
// Metadata: 'LabelMetadata', // Legacy
MoviesAndShows: 'MoviesAndShows',
Music: 'TabMusic',
Subtitles: 'Subtitles',
Other: 'Other'
export const CATEGORY_LABELS: Record<PluginCategory, string> = {
[PluginCategory.Administration]: 'HeaderAdmin',
[PluginCategory.General]: 'General',
[PluginCategory.Anime]: 'Anime',
[PluginCategory.Books]: 'Books',
[PluginCategory.LiveTV]: 'LiveTV',
[PluginCategory.MoviesAndShows]: 'MoviesAndShows',
[PluginCategory.Music]: 'TabMusic',
[PluginCategory.Subtitles]: 'Subtitles',
[PluginCategory.Other]: 'Other'
};

View File

@@ -0,0 +1,12 @@
/** Supported plugin category values. */
export enum PluginCategory {
Administration = 'Administration',
General = 'General',
Anime = 'Anime',
Books = 'Books',
LiveTV = 'LiveTV',
MoviesAndShows = 'MoviesAndShows',
Music = 'Music',
Subtitles = 'Subtitles',
Other = 'Other'
}

View File

@@ -1,20 +1,22 @@
import Settings from '@mui/icons-material/Settings';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid2';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import React, { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePackages } from 'apps/dashboard/features/plugins/api/usePackages';
import PackageCard from 'apps/dashboard/features/plugins/components/PackageCard';
import { PluginCategory } from 'apps/dashboard/features/plugins/constants/pluginCategory';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
import getPackageCategories from 'apps/dashboard/features/plugins/utils/getPackageCategories';
import getPackagesByCategory from 'apps/dashboard/features/plugins/utils/getPackagesByCategory';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { usePackages } from 'apps/dashboard/features/plugins/api/usePackages';
import Loading from 'components/loading/LoadingComponent';
import getPackageCategories from 'apps/dashboard/features/plugins/utils/getPackageCategories';
import Stack from '@mui/material/Stack';
import getPackagesByCategory from 'apps/dashboard/features/plugins/utils/getPackagesByCategory';
import PackageCard from 'apps/dashboard/features/plugins/components/PackageCard';
import Grid from '@mui/material/Grid2';
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import Settings from '@mui/icons-material/Settings';
import { Link } from 'react-router-dom';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
export const Component = () => {
const { data: packages, isPending: isPackagesPending } = usePackages();
@@ -31,7 +33,7 @@ export const Component = () => {
}, []);
const getCategoryLabel = (category: string) => {
const categoryKey = category.replace(/\s/g, '');
const categoryKey = category.replace(/\s/g, '') as PluginCategory;
if (CATEGORY_LABELS[categoryKey]) {
return globalize.translate(CATEGORY_LABELS[categoryKey]);

View File

@@ -1,34 +1,42 @@
import Settings from '@mui/icons-material/Settings';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid2';
import IconButton from '@mui/material/IconButton/IconButton';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import React, { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import SearchInput from 'apps/dashboard/components/SearchInput';
import { usePluginDetails } from 'apps/dashboard/features/plugins/api/usePluginDetails';
import PluginCard from 'apps/dashboard/features/plugins/components/PluginCard';
import { PluginCategory } from 'apps/dashboard/features/plugins/constants/pluginCategory';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import { usePluginDetails } from 'apps/dashboard/features/plugins/api/usePluginDetails';
import { Link } from 'react-router-dom';
import IconButton from '@mui/material/IconButton/IconButton';
import Settings from '@mui/icons-material/Settings';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
import SearchInput from 'apps/dashboard/components/SearchInput';
/**
* The list of primary/main categories.
* Any category not in this list will be added to the "other" category.
*/
const MAIN_CATEGORIES = [
'administration',
'general',
'anime',
'books',
'livetv',
'moviesandshows',
'music',
'subtitles'
PluginCategory.Administration.toLowerCase(),
PluginCategory.General.toLowerCase(),
PluginCategory.Anime.toLowerCase(),
PluginCategory.Books.toLowerCase(),
PluginCategory.LiveTV.toLowerCase(),
PluginCategory.MoviesAndShows.toLowerCase(),
PluginCategory.Music.toLowerCase(),
PluginCategory.Subtitles.toLowerCase()
];
/** The installed meta category. */
const INSTALLED_CATEGORY = 'installed';
export const Component = () => {
const {
data: pluginDetails,
@@ -47,18 +55,19 @@ export const Component = () => {
let filtered = pluginDetails;
if (category) {
if (category === 'installed') {
if (category === INSTALLED_CATEGORY) {
// Installed plugins will have a status
filtered = filtered.filter(p => p.status);
} else if (category === 'other') {
} else if (category === PluginCategory.Other.toLowerCase()) {
filtered = filtered.filter(p => (
p.category && !MAIN_CATEGORIES.includes(p.category.toLocaleLowerCase())
p.category && !MAIN_CATEGORIES.includes(p.category.toLowerCase())
));
} else {
filtered = filtered.filter(p => p.category?.toLocaleLowerCase() === category);
filtered = filtered.filter(p => p.category?.toLowerCase() === category);
}
}
return filtered
.filter(i => i.name?.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()));
.filter(i => i.name?.toLowerCase().includes(searchQuery.toLowerCase()));
} else {
return [];
}
@@ -147,21 +156,21 @@ export const Component = () => {
/>
<Chip
color={category === 'installed' ? 'primary' : undefined}
color={category === INSTALLED_CATEGORY ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setCategory('installed')}
onClick={() => setCategory(INSTALLED_CATEGORY)}
label={globalize.translate('LabelInstalled')}
/>
<Divider orientation='vertical' flexItem />
{Object.keys(CATEGORY_LABELS).map(c => (
{Object.values(PluginCategory).map(c => (
<Chip
key={c}
color={category === c.toLocaleLowerCase() ? 'primary' : undefined}
color={category === c.toLowerCase() ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setCategory(c.toLocaleLowerCase())}
label={globalize.translate(CATEGORY_LABELS[c])}
onClick={() => setCategory(c.toLowerCase())}
label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])}
/>
))}
</Stack>