From 4fde128fc72e342e333dd34ad4139de5a14f056c Mon Sep 17 00:00:00 2001 From: Ryan Dawes <105608038+dawescc@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:38:28 -0500 Subject: [PATCH] Feature: Add indexer flags to results (#539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display Prowlarr indexer flags by rendering them as distinct, color-coded badges. - [New] TAGS Render Type: Added support for a TAGS column type that renders a list of strings as distinct badges. - Updated `ReleaseCell` to handle the TAGS type: - Desktop: Renders distinct badges side-by-side. - Mobile: Renders as a comma-separated text list (e.g., "FREELEECH, DOUBLE UPLOAD"). - Styling: Added dynamic colors for common flags: - Freeleech → Green - Double Upload → Blue - VIP → Amber - Sticky → Yellow - Prowlarr Source: Updated the "Flags" column to use the new TAGS render type, enable uppercase styling, and show on mobile devices. Desktop screenshot: CleanShot 2026-01-25 at 21 32 24@2x Mobile screenshot: image --- shelfmark/release_sources/__init__.py | 1 + shelfmark/release_sources/prowlarr/source.py | 14 +- src/frontend/src/components/ReleaseCell.tsx | 40 +++ src/frontend/src/components/ReleaseModal.tsx | 303 +++++++++---------- src/frontend/src/types/index.ts | 2 +- src/frontend/src/utils/colorMaps.ts | 15 + 6 files changed, 218 insertions(+), 157 deletions(-) diff --git a/shelfmark/release_sources/__init__.py b/shelfmark/release_sources/__init__.py index 34e3f9fc..e5e39de6 100644 --- a/shelfmark/release_sources/__init__.py +++ b/shelfmark/release_sources/__init__.py @@ -58,6 +58,7 @@ class ColumnRenderType(str, Enum): """How the frontend should render the column value.""" TEXT = "text" # Plain text BADGE = "badge" # Colored badge (format, language) + TAGS = "tags" # List of colored badges SIZE = "size" # File size formatting NUMBER = "number" # Numeric value PEERS = "peers" # Peers display: "S/L" with color based on seeder count diff --git a/shelfmark/release_sources/prowlarr/source.py b/shelfmark/release_sources/prowlarr/source.py index 19da899a..f597716d 100644 --- a/shelfmark/release_sources/prowlarr/source.py +++ b/shelfmark/release_sources/prowlarr/source.py @@ -199,6 +199,7 @@ def _prowlarr_result_to_release(result: dict, search_content_type: str = "ebook" "indexer_id": result.get("indexerId"), "files": result.get("files"), "grabs": result.get("grabs"), + "indexer_flags": result.get("indexerFlags", []), }, ) @@ -248,6 +249,17 @@ class ProwlarrSource(ReleaseSource): sortable=True, sort_key="seeders", ), + ColumnSchema( + key="extra.indexer_flags", + label="Flags", + render_type=ColumnRenderType.TAGS, + align=ColumnAlign.CENTER, + width="minmax(80px, 1.5fr)", + hide_mobile=False, + color_hint=ColumnColorHint(type="map", value="flags"), + fallback="-", + uppercase=True, + ), ColumnSchema( key="content_type", label="Type", @@ -270,7 +282,7 @@ class ProwlarrSource(ReleaseSource): sort_key="size_bytes", ), ], - grid_template="minmax(0,2fr) minmax(80px,1fr) 60px 70px 90px 80px", + grid_template="minmax(0,2fr) minmax(80px,1fr) 60px 70px 80px 90px 80px", leading_cell=LeadingCellConfig(type=LeadingCellType.NONE), # No leading cell for Prowlarr supported_filters=["language"], # Enables multi-language query expansion; Prowlarr language metadata is unreliable ) diff --git a/src/frontend/src/components/ReleaseCell.tsx b/src/frontend/src/components/ReleaseCell.tsx index b9173b4e..f56001cb 100644 --- a/src/frontend/src/components/ReleaseCell.tsx +++ b/src/frontend/src/components/ReleaseCell.tsx @@ -50,6 +50,46 @@ export const ReleaseCell = ({ column, release, compact = false, onlineServers }: ); } + case 'tags': { + // Tags display: render list of strings as distinct badges + // Treat non-array values as single-item array + const tags = (Array.isArray(rawValue) ? rawValue : (value ? [value] : [])) as any[]; + + // Compact mode: render as comma-separated text + if (compact) { + if (tags.length === 0) return {column.fallback}; + const displayTags = column.uppercase ? tags.map(t => String(t).toUpperCase()) : tags; + return {displayTags.join(', ')}; + } + + if (!tags.length) { + return ( +
+ {column.fallback} +
+ ); + } + + return ( +
+ {tags.map((tag, idx) => { + const tagStr = String(tag); + const displayTag = column.uppercase ? tagStr.toUpperCase() : tagStr; + const colorStyle = getColorStyleFromHint(tagStr, column.color_hint); + + return ( + + {displayTag} + + ); + })} +
+ ); + } + case 'size': if (compact) { return {displayValue}; diff --git a/src/frontend/src/components/ReleaseModal.tsx b/src/frontend/src/components/ReleaseModal.tsx index ac98c4a5..cc826aba 100644 --- a/src/frontend/src/components/ReleaseModal.tsx +++ b/src/frontend/src/components/ReleaseModal.tsx @@ -1248,8 +1248,8 @@ export const ReleaseModal = ({ - {/* Scrollable content */} -
+ {/* Scrollable content */} +
{/* Book summary - scrolls with content */}
{book.preview ? ( @@ -1391,18 +1391,17 @@ export const ReleaseModal = ({ }} /> {allTabs.map((tab) => ( - + > + {tab.displayName} + ))}
@@ -1423,9 +1422,8 @@ export const ReleaseModal = ({ return next; }); }} - className={`p-2.5 rounded-full transition-colors hover-surface text-gray-500 dark:text-gray-400 ${ - manualQuery.trim() ? 'text-emerald-600 dark:text-emerald-400' : '' - }`} + className={`p-2.5 rounded-full transition-colors hover-surface text-gray-500 dark:text-gray-400 ${manualQuery.trim() ? 'text-emerald-600 dark:text-emerald-400' : '' + }`} aria-label="Manual search query" title="Manual query" > @@ -1444,9 +1442,8 @@ export const ReleaseModal = ({ + ); + }} + > + {({ close }) => ( +
+ {columnConfig.supported_filters?.includes('format') && availableFormats.length > 0 && ( + setFormatFilter(typeof val === 'string' ? val : val[0] ?? '')} + placeholder="All Formats" + /> )} - - ); - }} - > - {({ close }) => ( -
- {columnConfig.supported_filters?.includes('format') && availableFormats.length > 0 && ( - setFormatFilter(typeof val === 'string' ? val : val[0] ?? '')} - placeholder="All Formats" - /> + {columnConfig.supported_filters?.includes('language') && ( + + )} + {/* Apply button - re-fetch with server-side filters/expansion (e.g. language-aware searches) */} + {(activeTab === 'direct_download' || activeTab === 'prowlarr') && ( + + )} +
)} - {columnConfig.supported_filters?.includes('language') && ( - - )} - {/* Apply button - re-fetch with server-side filters/expansion (e.g. language-aware searches) */} - {(activeTab === 'direct_download' || activeTab === 'prowlarr') && ( - - )} -
+ )} - - )}
)} @@ -1691,11 +1685,10 @@ export const ReleaseModal = ({ @@ -1731,16 +1724,16 @@ export const ReleaseModal = ({ releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type ?? '' ) )) && ( -
- -
- )} +
+ +
+ )} ) : ( <> @@ -1768,22 +1761,22 @@ export const ReleaseModal = ({ releasesBySource[activeTab]?.search_info?.[activeTab]?.search_type ?? '' ) )) && ( -
- -
- )} + + + )} {/* Expanding search - show skeleton below existing results */} {currentTabLoading && filteredReleases.length > 0 && ( diff --git a/src/frontend/src/types/index.ts b/src/frontend/src/types/index.ts index 15eae8d8..770ad5b4 100644 --- a/src/frontend/src/types/index.ts +++ b/src/frontend/src/types/index.ts @@ -204,7 +204,7 @@ export interface ReleaseSource { } // Column schema types for plugin-driven release list UI -export type ColumnRenderType = 'text' | 'badge' | 'size' | 'number' | 'peers'; +export type ColumnRenderType = 'text' | 'badge' | 'tags' | 'size' | 'number' | 'peers'; export type ColumnAlign = 'left' | 'center' | 'right'; export interface ColumnColorHint { diff --git a/src/frontend/src/utils/colorMaps.ts b/src/frontend/src/utils/colorMaps.ts index 9e92d6b3..7f7fdc90 100644 --- a/src/frontend/src/utils/colorMaps.ts +++ b/src/frontend/src/utils/colorMaps.ts @@ -51,10 +51,18 @@ const CONTENT_TYPE_COLORS: Record = { audiobook: { bg: 'bg-violet-500/20', text: 'text-violet-700 dark:text-violet-300' }, }; +const FLAG_COLORS: Record = { + freeleech: { bg: 'bg-green-500/20', text: 'text-green-700 dark:text-green-300' }, + 'double upload': { bg: 'bg-blue-500/20', text: 'text-blue-700 dark:text-blue-300' }, + vip: { bg: 'bg-amber-500/20', text: 'text-amber-700 dark:text-amber-300' }, + sticky: { bg: 'bg-yellow-500/20', text: 'text-yellow-700 dark:text-yellow-300' }, +}; + const DEFAULT_FORMAT_COLOR: ColorStyle = { bg: 'bg-cyan-500/20', text: 'text-cyan-700 dark:text-cyan-300' }; const DEFAULT_LANGUAGE_COLOR: ColorStyle = { bg: 'bg-indigo-500/20', text: 'text-indigo-700 dark:text-indigo-300' }; const DEFAULT_DOWNLOAD_TYPE_COLOR: ColorStyle = { bg: 'bg-violet-500/20', text: 'text-violet-700 dark:text-violet-300' }; const DEFAULT_CONTENT_TYPE_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' }; +const DEFAULT_FLAG_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' }; const FALLBACK_COLOR: ColorStyle = { bg: 'bg-gray-500/20', text: 'text-gray-700 dark:text-gray-300' }; export function getFormatColor(format?: string): ColorStyle { @@ -77,6 +85,11 @@ export function getContentTypeColor(contentType?: string): ColorStyle { return CONTENT_TYPE_COLORS[contentType.toLowerCase()] || DEFAULT_CONTENT_TYPE_COLOR; } +export function getFlagColor(flag?: string): ColorStyle { + if (!flag || flag === '-') return FALLBACK_COLOR; + return FLAG_COLORS[flag.toLowerCase()] || DEFAULT_FLAG_COLOR; +} + /** * Color hint type for dynamic column coloring. */ @@ -106,6 +119,8 @@ export function getColorStyleFromHint(value: string, colorHint?: ColumnColorHint return getDownloadTypeColor(value); case 'content_type': return getContentTypeColor(value); + case 'flags': + return getFlagColor(value); default: return FALLBACK_COLOR; }