1- import React , { useState } from 'react'
2- import { TextField , Button , List , ListItem , ListItemText , CircularProgress , Typography , Divider , ListItemSecondaryAction , IconButton } from '@material-ui/core'
1+ import React , { useState , useMemo } from 'react'
2+ import {
3+ TextField ,
4+ Button ,
5+ List ,
6+ ListItem ,
7+ ListItemText ,
8+ CircularProgress ,
9+ Typography ,
10+ Divider ,
11+ ListItemSecondaryAction ,
12+ IconButton ,
13+ Select ,
14+ MenuItem ,
15+ FormControl ,
16+ InputLabel ,
17+ useMediaQuery ,
18+ } from '@material-ui/core'
319import { useTranslation } from 'react-i18next'
420import axios from 'axios'
521import { torznabSearchHost } from 'utils/Hosts'
6- import { AddCircleOutline as AddIcon } from '@material-ui/icons'
22+ import { AddCircleOutline as AddIcon , ArrowUpward , ArrowDownward } from '@material-ui/icons'
23+ import { parseSizeToBytes , formatSizeToClassicUnits } from 'utils/Utils'
724
825export default function TorznabSearch ( { onSelect } ) {
926 const { t } = useTranslation ( )
1027 const [ query , setQuery ] = useState ( '' )
1128 const [ results , setResults ] = useState ( [ ] )
1229 const [ loading , setLoading ] = useState ( false )
1330 const [ searched , setSearched ] = useState ( false )
31+ const [ sortField , setSortField ] = useState ( '' ) // '', 'size', 'seeds', 'peers'
32+ const [ sortDirection , setSortDirection ] = useState ( 'desc' ) // 'asc' or 'desc'
33+ const isMobile = useMediaQuery ( '(max-width:600px)' )
1434
1535 const handleSearch = async ( ) => {
1636 if ( ! query ) return
@@ -27,56 +47,178 @@ export default function TorznabSearch({ onSelect }) {
2747 }
2848 }
2949
30- const handleKeyDown = ( e ) => {
50+ const handleKeyDown = e => {
3151 if ( e . key === 'Enter' ) {
3252 handleSearch ( )
3353 }
3454 }
3555
56+ const toggleSortDirection = ( ) => {
57+ setSortDirection ( prev => ( prev === 'asc' ? 'desc' : 'asc' ) )
58+ }
59+
60+ const sortedResults = useMemo ( ( ) => {
61+ if ( ! sortField || results . length === 0 ) return results
62+
63+ const sorted = [ ...results ] . sort ( ( a , b ) => {
64+ let aVal
65+ let bVal
66+
67+ switch ( sortField ) {
68+ case 'size' :
69+ aVal = parseSizeToBytes ( a . Size || '0' )
70+ bVal = parseSizeToBytes ( b . Size || '0' )
71+ break
72+ case 'seeds' :
73+ aVal = a . Seed || 0
74+ bVal = b . Seed || 0
75+ break
76+ case 'peers' :
77+ aVal = a . Peer || 0
78+ bVal = b . Peer || 0
79+ break
80+ default :
81+ return 0
82+ }
83+
84+ if ( aVal === bVal ) return 0
85+ return sortDirection === 'asc' ? ( aVal < bVal ? - 1 : 1 ) : aVal > bVal ? - 1 : 1
86+ } )
87+
88+ return sorted
89+ } , [ results , sortField , sortDirection ] )
90+
3691 return (
3792 < div style = { { marginTop : '1.5em' } } >
38- < div style = { { display : 'flex' , gap : '8px' } } >
93+ < div style = { { display : 'flex' , gap : '8px' , flexWrap : isMobile ? 'wrap' : 'nowrap' } } >
3994 < TextField
4095 label = { t ( 'Torznab.SearchTorznab' ) }
4196 value = { query }
42- onChange = { ( e ) => setQuery ( e . target . value ) }
97+ onChange = { e => setQuery ( e . target . value ) }
4398 onKeyDown = { handleKeyDown }
44- variant = " outlined"
45- size = " small"
99+ variant = ' outlined'
100+ size = ' small'
46101 fullWidth
47102 placeholder = { t ( 'Torznab.SearchMoviesShows' ) }
103+ style = { { flex : isMobile ? '1 1 100%' : '1' } }
48104 />
49- < Button variant = "contained" color = "primary" onClick = { handleSearch } disabled = { loading } style = { { minWidth : '80px' } } >
50- { loading ? < CircularProgress size = { 24 } color = "inherit" /> : t ( 'Torznab.SearchTorrents' ) }
105+ < Button
106+ variant = 'contained'
107+ color = 'primary'
108+ onClick = { handleSearch }
109+ disabled = { loading }
110+ style = { {
111+ minWidth : isMobile ? '100%' : '80px' ,
112+ flex : isMobile ? '1 1 100%' : '0 0 auto' ,
113+ } }
114+ >
115+ { loading ? < CircularProgress size = { 24 } color = 'inherit' /> : t ( 'Torznab.SearchTorrents' ) }
51116 </ Button >
52117 </ div >
53118 { searched && (
54- < div style = { { maxHeight : '200px' , overflowY : 'auto' , marginTop : '8px' , border : '1px solid rgba(0,0,0,0.12)' , borderRadius : '4px' } } >
55- { results . length === 0 ? (
56- < div style = { { padding : '8px' , textAlign : 'center' } } >
57- < Typography variant = "body2" > { loading ? t ( 'Torznab.SearchTorrents' ) : t ( 'Torznab.NoResultsFound' ) } </ Typography >
119+ < div style = { { marginTop : '8px' } } >
120+ { results . length > 0 && (
121+ < div
122+ style = { {
123+ display : 'flex' ,
124+ gap : isMobile ? '8px' : '4px' ,
125+ marginBottom : '12px' ,
126+ alignItems : 'center' ,
127+ padding : isMobile ? '12px 8px' : '8px 12px' ,
128+ backgroundColor : 'rgba(0, 0, 0, 0.02)' ,
129+ borderRadius : '4px' ,
130+ border : '1px solid rgba(0, 0, 0, 0.08)' ,
131+ flexWrap : isMobile ? 'wrap' : 'nowrap' ,
132+ } }
133+ >
134+ < FormControl
135+ variant = 'outlined'
136+ size = 'small'
137+ style = { {
138+ minWidth : isMobile ? '100%' : 140 ,
139+ flexShrink : 0 ,
140+ flex : isMobile ? '1 1 100%' : '0 0 auto' ,
141+ } }
142+ >
143+ < InputLabel > { t ( 'Torznab.SortBy' ) } </ InputLabel >
144+ < Select value = { sortField } onChange = { e => setSortField ( e . target . value ) } label = { t ( 'Torznab.SortBy' ) } >
145+ < MenuItem value = '' > { t ( 'Torznab.SortByNone' ) } </ MenuItem >
146+ < MenuItem value = 'size' > { t ( 'Torznab.SortBySize' ) } </ MenuItem >
147+ < MenuItem value = 'seeds' > { t ( 'Torznab.SortBySeeds' ) } </ MenuItem >
148+ < MenuItem value = 'peers' > { t ( 'Torznab.SortByPeers' ) } </ MenuItem >
149+ </ Select >
150+ </ FormControl >
151+ { sortField && (
152+ < IconButton
153+ size = 'small'
154+ onClick = { toggleSortDirection }
155+ title = { sortDirection === 'asc' ? t ( 'Torznab.SortAscending' ) : t ( 'Torznab.SortDescending' ) }
156+ style = { {
157+ marginLeft : isMobile ? 'auto' : '4px' ,
158+ padding : '8px' ,
159+ } }
160+ >
161+ { sortDirection === 'asc' ? < ArrowUpward /> : < ArrowDownward /> }
162+ </ IconButton >
163+ ) }
58164 </ div >
59- ) : (
60- < List dense >
61- { results . map ( ( item , index ) => (
62- < React . Fragment key = { item . Hash || item . Link || index } >
63- < ListItem button onClick = { ( ) => onSelect ( item . Magnet || item . Link ) } >
64- < ListItemText
65- primary = { item . Title }
66- secondary = { `${ item . Size } • S:${ item . Seed } P:${ item . Peer } ` }
67- primaryTypographyProps = { { noWrap : true , style : { fontSize : '0.9rem' } } }
68- />
69- < ListItemSecondaryAction >
70- < IconButton edge = "end" aria-label = "add" onClick = { ( ) => onSelect ( item . Magnet || item . Link ) } >
71- < AddIcon />
72- </ IconButton >
73- </ ListItemSecondaryAction >
74- </ ListItem >
75- < Divider />
76- </ React . Fragment >
77- ) ) }
78- </ List >
79165 ) }
166+ < div
167+ style = { {
168+ maxHeight : isMobile ? '300px' : '200px' ,
169+ overflowY : 'auto' ,
170+ border : '1px solid rgba(0,0,0,0.12)' ,
171+ borderRadius : '4px' ,
172+ } }
173+ >
174+ { results . length === 0 ? (
175+ < div style = { { padding : '8px' , textAlign : 'center' } } >
176+ < Typography variant = 'body2' >
177+ { loading ? t ( 'Torznab.SearchTorrents' ) : t ( 'Torznab.NoResultsFound' ) }
178+ </ Typography >
179+ </ div >
180+ ) : (
181+ < List dense >
182+ { sortedResults . map ( ( item , index ) => {
183+ const sizeBytes = parseSizeToBytes ( item . Size || '0' )
184+ const formattedSize = formatSizeToClassicUnits ( sizeBytes )
185+ return (
186+ < React . Fragment key = { item . Hash || item . Link || index } >
187+ < ListItem button onClick = { ( ) => onSelect ( item . Magnet || item . Link ) } >
188+ < ListItemText
189+ primary = { item . Title }
190+ secondary = { `${ formattedSize } • S:${ item . Seed || 0 } P:${ item . Peer || 0 } ` }
191+ primaryTypographyProps = { {
192+ noWrap : ! isMobile ,
193+ style : {
194+ fontSize : isMobile ? '0.85rem' : '0.9rem' ,
195+ whiteSpace : isMobile ? 'normal' : 'nowrap' ,
196+ } ,
197+ } }
198+ secondaryTypographyProps = { {
199+ style : {
200+ fontSize : isMobile ? '0.75rem' : '0.8rem' ,
201+ } ,
202+ } }
203+ />
204+ < ListItemSecondaryAction >
205+ < IconButton
206+ edge = 'end'
207+ aria-label = 'add'
208+ onClick = { ( ) => onSelect ( item . Magnet || item . Link ) }
209+ size = { isMobile ? 'small' : 'medium' }
210+ >
211+ < AddIcon />
212+ </ IconButton >
213+ </ ListItemSecondaryAction >
214+ </ ListItem >
215+ < Divider />
216+ </ React . Fragment >
217+ )
218+ } ) }
219+ </ List >
220+ ) }
221+ </ div >
80222 </ div >
81223 ) }
82224 </ div >
0 commit comments