From 923ab1ebd8ce0abb3754501af71457b8f047d37b Mon Sep 17 00:00:00 2001 From: Samuel Chau Date: Fri, 7 Mar 2025 18:47:47 +1030 Subject: [PATCH] feat: format selection and scoring customization options (#153) - new selection resolver allowing users to add formats with any score (including 0) - basic / advanced view for format selector - seperate formatGroups into shared constants file --- .../src/components/profile/ProfileModal.jsx | 80 +++- .../profile/scoring/AdvancedView.jsx | 184 +++------ .../components/profile/scoring/BasicView.jsx | 35 +- .../profile/scoring/FormatSelector.jsx | 75 ++++ .../profile/scoring/FormatSelectorModal.jsx | 235 ++++++++++++ .../profile/scoring/FormatSettings.jsx | 354 +++++++++++++----- .../profile/scoring/ProfileScoringTab.jsx | 7 +- .../profile/scoring/SelectiveView.jsx | 219 +++++++++++ frontend/src/constants/formatGroups.js | 167 +++++++++ 9 files changed, 1108 insertions(+), 248 deletions(-) create mode 100644 frontend/src/components/profile/scoring/FormatSelector.jsx create mode 100644 frontend/src/components/profile/scoring/FormatSelectorModal.jsx create mode 100644 frontend/src/components/profile/scoring/SelectiveView.jsx create mode 100644 frontend/src/constants/formatGroups.js diff --git a/frontend/src/components/profile/ProfileModal.jsx b/frontend/src/components/profile/ProfileModal.jsx index 584a42d..9ca4099 100644 --- a/frontend/src/components/profile/ProfileModal.jsx +++ b/frontend/src/components/profile/ProfileModal.jsx @@ -337,20 +337,74 @@ function ProfileModal({ minCustomFormatScore, upgradeUntilScore, minScoreIncrement, - custom_formats: customFormats - .filter(format => format.score !== 0) - .sort((a, b) => { - // First sort by score (descending) - if (b.score !== a.score) { - return b.score - a.score; + custom_formats: (() => { + // Check if selective mode is enabled + const selectiveMode = localStorage.getItem('formatSettingsSelectiveMode'); + const useSelectiveMode = selectiveMode !== null && JSON.parse(selectiveMode); + + if (useSelectiveMode) { + // In selective mode, save both: + // 1. Formats with non-zero scores as usual + // 2. Formats with zero score that have been explicitly selected in selectedFormatIds + + try { + // Get the list of explicitly selected format IDs + const selectedFormatIdsStr = localStorage.getItem('selectedFormatIds'); + const selectedFormatIds = selectedFormatIdsStr ? JSON.parse(selectedFormatIdsStr) : []; + + // Get formats with non-zero scores + const nonZeroFormats = customFormats.filter(format => format.score !== 0); + + // Get formats with zero scores that are explicitly selected + const explicitlySelectedZeroFormats = customFormats.filter(format => + format.score === 0 && selectedFormatIds.includes(format.id) + ); + + // Combine both lists + return [...nonZeroFormats, ...explicitlySelectedZeroFormats] + .sort((a, b) => { + // First sort by score (descending) + if (b.score !== a.score) { + return b.score - a.score; + } + // Then alphabetically for equal scores + return a.name.localeCompare(b.name); + }) + .map(format => ({ + name: format.name, + score: format.score + })); + } catch (e) { + // If there's any error parsing the selectedFormatIds, fall back to just non-zero scores + return customFormats + .filter(format => format.score !== 0) + .sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + return a.name.localeCompare(b.name); + }) + .map(format => ({ + name: format.name, + score: format.score + })); } - // Then alphabetically for equal scores - return a.name.localeCompare(b.name); - }) - .map(format => ({ - name: format.name, - score: format.score - })), + } else { + // Standard behavior - only include formats with non-zero scores + return customFormats + .filter(format => format.score !== 0) + .sort((a, b) => { + // First sort by score (descending) + if (b.score !== a.score) { + return b.score - a.score; + } + // Then alphabetically for equal scores + return a.name.localeCompare(b.name); + }) + .map(format => ({ + name: format.name, + score: format.score + })); + } + })(), qualities: sortedQualities .filter(q => q.enabled) .map(q => { diff --git a/frontend/src/components/profile/scoring/AdvancedView.jsx b/frontend/src/components/profile/scoring/AdvancedView.jsx index 51edad8..2cfd114 100644 --- a/frontend/src/components/profile/scoring/AdvancedView.jsx +++ b/frontend/src/components/profile/scoring/AdvancedView.jsx @@ -1,138 +1,34 @@ -import React from 'react'; +import React, { useMemo } 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'; +import { X } from 'lucide-react'; +import { groupFormatsByTags, getGroupIcon } from '@constants/formatGroups'; -const AdvancedView = ({formats, onScoreChange}) => { +const AdvancedView = ({formats, onScoreChange, onFormatRemove, showRemoveButton}) => { 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') - ); + // Use the shared helper function to group formats + const formatGroups = groupFormatsByTags(formats); - 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: , - HDR: , - 'Release Groups': , - 'Group Tier Lists': , - 'Streaming Services': , - Codecs: , - Storage: , - Resolution: , - Language: , - Source: