Files
profilarr/frontend/src/components/profile/scoring/AdvancedView.jsx
santiagosayshey ca1c2bf777 feat: format view improvements (#148)
- feat: seperate group tier lists into seperate category, hide groups with no formats
- style: adjust NumberInput width and text alignment for better usability
2025-02-24 03:11:51 +10:30

201 lines
8.0 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import NumberInput from '@ui/NumberInput';
import {useSorting} from '@hooks/useSorting';
import SortDropdown from '@ui/SortDropdown';
import {
Music,
Tv,
Users,
Cloud,
Film,
HardDrive,
Maximize,
Globe,
Video,
Flag,
Zap,
Package,
List
} from 'lucide-react';
const AdvancedView = ({formats, onScoreChange}) => {
const sortOptions = [
{label: 'Name', value: 'name'},
{label: 'Score', value: 'score'}
];
// Group formats by their tags
const groupedFormats = formats.reduce((acc, format) => {
// Check if format has any tags that match our known categories
const hasKnownTag = format.tags?.some(
tag =>
tag.includes('Audio') ||
tag.includes('Codec') ||
tag.includes('Enhancement') ||
tag.includes('HDR') ||
tag.includes('Flag') ||
tag.includes('Language') ||
(tag.includes('Release Group') && !tag.includes('Tier')) ||
tag.includes('Release Group Tier') ||
tag.includes('Resolution') ||
tag.includes('Source') ||
tag.includes('Storage') ||
tag.includes('Streaming Service')
);
if (!hasKnownTag) {
if (!acc['Uncategorized']) acc['Uncategorized'] = [];
acc['Uncategorized'].push(format);
return acc;
}
format.tags.forEach(tag => {
if (!acc[tag]) acc[tag] = [];
acc[tag].push(format);
});
return acc;
}, {});
const formatGroups = {
Audio: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Audio'))
.flatMap(([_, formats]) => formats),
Codecs: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Codec'))
.flatMap(([_, formats]) => formats),
Enhancements: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Enhancement'))
.flatMap(([_, formats]) => formats),
HDR: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('HDR'))
.flatMap(([_, formats]) => formats),
'Indexer Flags': Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Flag'))
.flatMap(([_, formats]) => formats),
Language: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Language'))
.flatMap(([_, formats]) => formats),
'Release Groups': Object.entries(groupedFormats)
.filter(
([tag]) =>
tag.includes('Release Group') && !tag.includes('Tier')
)
.flatMap(([_, formats]) => formats),
'Group Tier Lists': Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Release Group Tier'))
.flatMap(([_, formats]) => formats),
Resolution: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Resolution'))
.flatMap(([_, formats]) => formats),
Source: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Source'))
.flatMap(([_, formats]) => formats),
Storage: Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Storage'))
.flatMap(([_, formats]) => formats),
'Streaming Services': Object.entries(groupedFormats)
.filter(([tag]) => tag.includes('Streaming Service'))
.flatMap(([_, formats]) => formats),
Uncategorized: groupedFormats['Uncategorized'] || []
};
const getGroupIcon = groupName => {
const icons = {
Audio: <Music size={16} />,
HDR: <Tv size={16} />,
'Release Groups': <Users size={16} />,
'Group Tier Lists': <List size={16} />,
'Streaming Services': <Cloud size={16} />,
Codecs: <Film size={16} />,
Storage: <HardDrive size={16} />,
Resolution: <Maximize size={16} />,
Language: <Globe size={16} />,
Source: <Video size={16} />,
'Indexer Flags': <Flag size={16} />,
Enhancements: <Zap size={16} />,
Uncategorized: <Package size={16} />
};
return icons[groupName] || <Package size={16} />;
};
// Create sort instances for each group
const groupSorts = Object.entries(formatGroups)
.filter(([_, formats]) => formats.length > 0) // Only create sorts for non-empty groups
.reduce((acc, [groupName, formats]) => {
const defaultSort = {field: 'name', direction: 'asc'};
const {sortConfig, updateSort, sortData} = useSorting(defaultSort);
acc[groupName] = {
sortedData: sortData(formats),
sortConfig,
updateSort
};
return acc;
}, {});
return (
<div className='grid grid-cols-1 md:grid-cols-2 gap-3'>
{Object.entries(formatGroups)
.filter(([_, formats]) => formats.length > 0) // Only render non-empty groups
.sort(([a], [b]) => a.localeCompare(b))
.map(([groupName, formats]) => {
const {sortedData, sortConfig, updateSort} =
groupSorts[groupName];
return (
<div
key={groupName}
className='bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden'>
<div className='px-4 py-3 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center'>
<h3 className='text-sm font-bold text-gray-900 dark:text-gray-100 flex items-center'>
{getGroupIcon(groupName)}
<span className='ml-2'>{groupName}</span>
</h3>
<SortDropdown
sortOptions={sortOptions}
currentSort={sortConfig}
onSortChange={updateSort}
/>
</div>
<div className='divide-y divide-gray-200 dark:divide-gray-700'>
{sortedData.map(format => (
<div
key={format.id}
className='flex items-center justify-between px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-800/50 group'>
<div className='flex-1 min-w-0 mr-4'>
<p className='text-sm text-gray-900 dark:text-gray-100 truncate'>
{format.name}
</p>
</div>
<NumberInput
value={format.score}
onChange={value =>
onScoreChange(format.id, value)
}
/>
</div>
))}
</div>
</div>
);
})}
</div>
);
};
AdvancedView.propTypes = {
formats: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
score: PropTypes.number.isRequired,
tags: PropTypes.arrayOf(PropTypes.string)
})
).isRequired,
onScoreChange: PropTypes.func.isRequired
};
export default AdvancedView;