mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-20 05:56:10 +05:00
temporary disabled donation button behind the header. All torrents are rendered with card style. Created GRID wrapper for holding all torrent cards. Empty posters will be rendered as NoImageIcon svg
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { makeStyles, useTheme } from '@material-ui/core/styles'
|
||||
import { useTheme } from '@material-ui/core/styles'
|
||||
import Drawer from '@material-ui/core/Drawer'
|
||||
import AppBar from '@material-ui/core/AppBar'
|
||||
import Toolbar from '@material-ui/core/Toolbar'
|
||||
import List from '@material-ui/core/List'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
@@ -19,86 +18,22 @@ import ListItemText from '@material-ui/core/ListItemText'
|
||||
import ListIcon from '@material-ui/icons/List'
|
||||
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
|
||||
|
||||
import TorrentList from './TorrentList'
|
||||
import { Box } from '@material-ui/core'
|
||||
import TorrentList from '../TorrentList'
|
||||
|
||||
import AddDialogButton from './Add'
|
||||
import RemoveAll from './RemoveAll'
|
||||
import SettingsDialog from './Settings'
|
||||
import AboutDialog from './About'
|
||||
import { playlistAllHost, shutdownHost, torrserverHost } from '../utils/Hosts'
|
||||
import DonateDialog from './Donate'
|
||||
import UploadDialog from './Upload'
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 36,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: theme.spacing(7) + 1,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing(9) + 1,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
}))
|
||||
import AddDialogButton from '../Add'
|
||||
import RemoveAll from '../RemoveAll'
|
||||
import SettingsDialog from '../Settings'
|
||||
import AboutDialog from '../About'
|
||||
import { playlistAllHost, shutdownHost, torrserverHost } from '../../utils/Hosts'
|
||||
import DonateDialog from '../Donate'
|
||||
import UploadDialog from '../Upload'
|
||||
import useStyles from './useStyles'
|
||||
|
||||
export default function MiniDrawer() {
|
||||
const classes = useStyles()
|
||||
const theme = useTheme()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [tsVersion, setTSVersion] = React.useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [tsVersion, setTSVersion] = useState('')
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true)
|
||||
@@ -118,7 +53,6 @@ export default function MiniDrawer() {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="fixed"
|
||||
className={clsx(classes.appBar, {
|
||||
@@ -142,6 +76,7 @@ export default function MiniDrawer() {
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
className={clsx(classes.drawer, {
|
||||
@@ -156,9 +91,13 @@ export default function MiniDrawer() {
|
||||
}}
|
||||
>
|
||||
<div className={classes.toolbar}>
|
||||
<IconButton onClick={handleDrawerClose}>{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}</IconButton>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<AddDialogButton />
|
||||
<UploadDialog />
|
||||
@@ -170,10 +109,11 @@ export default function MiniDrawer() {
|
||||
<ListItemText primary="Playlist all torrents" />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<SettingsDialog />
|
||||
<DonateDialog />
|
||||
<AboutDialog />
|
||||
<ListItem button key="Close server" onClick={() => fetch(shutdownHost())}>
|
||||
<ListItemIcon>
|
||||
@@ -182,12 +122,15 @@ export default function MiniDrawer() {
|
||||
<ListItemText primary="Close server" />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
</Drawer>
|
||||
|
||||
<main className={classes.content}>
|
||||
<Box m="5em" />
|
||||
<div className={classes.toolbar} />
|
||||
|
||||
<TorrentList />
|
||||
</main>
|
||||
|
||||
<DonateDialog />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
web/src/components/Appbar/useStyles.js
Normal file
65
web/src/components/Appbar/useStyles.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
export default makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 36,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: theme.spacing(7) + 1,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing(9) + 1,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
}))
|
||||
@@ -20,21 +20,25 @@ export default function DonateDialog() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [snakeOpen, setSnakeOpen] = React.useState(true)
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
// NOT USED FOR NOW
|
||||
// const handleClickOpen = () => {
|
||||
// setOpen(true)
|
||||
// }
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem button key="Donate" onClick={handleClickOpen}>
|
||||
{/* !!!!!!!!!!! Should be removed or moved to sidebar because it is not visible. It is hiddent behind header */}
|
||||
{/* <ListItem button key="Donate" onClick={handleClickOpen}>
|
||||
<ListItemIcon>
|
||||
<CreditCardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Donate" />
|
||||
</ListItem>
|
||||
</ListItem> */}
|
||||
{/* !!!!!!!!!!!!!!!!!!!! */}
|
||||
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
|
||||
<DialogTitle id="form-dialog-title">Donate</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -63,12 +67,12 @@ export default function DonateDialog() {
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={snakeOpen}
|
||||
onClose={()=>{setSnakeOpen(false)}}
|
||||
onClose={() => { setSnakeOpen(false) }}
|
||||
autoHideDuration={6000}
|
||||
message="Donate?"
|
||||
action={
|
||||
<React.Fragment>
|
||||
<IconButton size="small" aria-label="close" color="inherit" onClick={()=>{
|
||||
<IconButton size="small" aria-label="close" color="inherit" onClick={() => {
|
||||
setSnakeOpen(false)
|
||||
setOpen(true)
|
||||
}}>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
|
||||
import 'fontsource-roboto'
|
||||
|
||||
import HeightIcon from '@material-ui/icons/Height';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
|
||||
import { getPeerString, humanizeSize } from '../utils/Utils'
|
||||
import { getPeerString, humanizeSize } from '../../utils/Utils'
|
||||
|
||||
import DialogTorrentInfo from './DialogTorrentInfo'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
import DialogCacheInfo from './DialogCacheInfo'
|
||||
import DialogTorrentInfo from '../DialogTorrentInfo'
|
||||
import { torrentsHost } from '../../utils/Hosts'
|
||||
import DialogCacheInfo from '../DialogCacheInfo'
|
||||
import DataUsageIcon from '@material-ui/icons/DataUsage'
|
||||
import { NoImageIcon } from '../../icons';
|
||||
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardDescriptionContent, TorrentCardDescriptionLabel, TorrentCardPoster } from './style';
|
||||
|
||||
export default function Torrent(props) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [showCache, setShowCache] = React.useState(false)
|
||||
const [torrent, setTorrent] = React.useState(props.torrent)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showCache, setShowCache] = useState(false)
|
||||
const [torrent, setTorrent] = useState(props.torrent)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,61 +43,74 @@ export default function Torrent(props) {
|
||||
}
|
||||
}, [torrent.hash, open])
|
||||
|
||||
const { title, name, poster, torrent_size, download_speed } = torrent
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem>
|
||||
<ButtonGroup style={{width:'100%',boxShadow:'2px 2px 2px gray'}} disableElevation variant="contained" color="primary">
|
||||
<Button
|
||||
style={{width: '100%', justifyContent:'start'}}
|
||||
onClick={() => {
|
||||
setShowCache(false)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
{torrent.poster &&
|
||||
<img src={torrent.poster} alt="" align="left" style={{width: 'auto',height:'100px',margin:'0 10px 0 0',borderRadius:'5px'}}/>
|
||||
}
|
||||
<Typography>
|
||||
{torrent.title ? torrent.title : torrent.name}
|
||||
{torrent.torrent_size > 0 ? ' | ' + humanizeSize(torrent.torrent_size) : ''}
|
||||
{torrent.download_speed > 0 ? ' | ' + humanizeSize(torrent.download_speed) + '/sec' : ''}
|
||||
{getPeerString(torrent) ? ' | ' + getPeerString(torrent) : '' }
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
<>
|
||||
|
||||
<TorrentCard>
|
||||
<TorrentCardPoster isPoster={poster}>
|
||||
{poster
|
||||
? <img src={poster} alt="poster" />
|
||||
: <NoImageIcon />}
|
||||
</TorrentCardPoster>
|
||||
|
||||
<TorrentCardButtons>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(true)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<DataUsageIcon />
|
||||
<Typography>Cache</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dropTorrent(torrent)
|
||||
}}
|
||||
Cache
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => dropTorrent(torrent)}
|
||||
>
|
||||
<CloseIcon />
|
||||
<Typography>Drop</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteTorrent(torrent)
|
||||
}}
|
||||
Drop
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => deleteTorrent(torrent)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<Typography>Delete</Typography>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ListItem>
|
||||
Delete
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(false)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<HeightIcon />
|
||||
Details
|
||||
</StyledButton>
|
||||
</TorrentCardButtons>
|
||||
|
||||
<TorrentCardDescription>
|
||||
<TorrentCardDescriptionLabel>Name</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{title || name}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Size</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{torrent_size > 0 && humanizeSize(torrent_size)}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Download speed</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{download_speed > 0 ? humanizeSize(download_speed) : '---'}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Peers</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{getPeerString(torrent) || '---'}</TorrentCardDescriptionContent>
|
||||
</TorrentCardDescription>
|
||||
</TorrentCard>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
aria-labelledby="form-dialog-title"
|
||||
fullWidth={true}
|
||||
fullWidth
|
||||
maxWidth={'lg'}
|
||||
>
|
||||
{!showCache ? <DialogTorrentInfo torrent={(open, torrent)} /> : <DialogCacheInfo hash={(open, torrent.hash)} />}
|
||||
@@ -105,15 +118,13 @@ export default function Torrent(props) {
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
98
web/src/components/Torrent/style.js
Normal file
98
web/src/components/Torrent/style.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const TorrentCard = styled.div`
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: 175px minmax(min-content, 1fr);
|
||||
grid-template-areas:
|
||||
"poster buttons"
|
||||
"description description";
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: #3fb57a;
|
||||
box-shadow:
|
||||
0px 2px 4px -1px rgb(0 0 0 / 20%),
|
||||
0px 4px 5px 0px rgb(0 0 0 / 14%),
|
||||
0px 1px 10px 0px rgb(0 0 0 / 12%);
|
||||
`
|
||||
|
||||
export const TorrentCardPoster = styled.div`
|
||||
grid-area: poster;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
${({ isPoster }) => isPoster ? css`
|
||||
img {
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
`: css`
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #74c39c;
|
||||
border: 1px solid;
|
||||
|
||||
svg {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
`};
|
||||
`
|
||||
export const TorrentCardButtons = styled.div`
|
||||
grid-area: buttons;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
`
|
||||
export const TorrentCardDescription = styled.div`
|
||||
grid-area: description;
|
||||
background: #74c39c;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
`
|
||||
|
||||
export const TorrentCardDescriptionLabel = styled.div`
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.4px;
|
||||
color: #216e47;
|
||||
`
|
||||
|
||||
export const TorrentCardDescriptionContent = styled.div`
|
||||
margin-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
export const StyledButton = styled.button`
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
background: #216e47;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
letter-spacing: 0.009em;
|
||||
|
||||
> :first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
font-size: 0.7rem;
|
||||
|
||||
> :first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:hover {
|
||||
background: #2a7e54;
|
||||
}
|
||||
`
|
||||
@@ -1,13 +1,18 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import Container from '@material-ui/core/Container'
|
||||
import styled from 'styled-components';
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Torrent from './Torrent'
|
||||
import List from '@material-ui/core/List'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
|
||||
export default function TorrentList(props, onChange) {
|
||||
const [torrents, setTorrents] = React.useState([])
|
||||
const [offline, setOffline] = React.useState(true)
|
||||
const TorrentListWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 350px));
|
||||
gap: 30px;
|
||||
`
|
||||
|
||||
export default function TorrentList() {
|
||||
const [torrents, setTorrents] = useState([])
|
||||
const [offline, setOffline] = useState(true)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,9 +30,11 @@ export default function TorrentList(props, onChange) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Container maxWidth="lg">{!offline ? <List>{torrents && torrents.map((torrent) => <Torrent key={torrent.hash} torrent={torrent} />)}</List> : <Typography>Offline</Typography>}</Container>
|
||||
</React.Fragment>
|
||||
<TorrentListWrapper>
|
||||
{offline ? <Typography>Offline</Typography> : (
|
||||
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
|
||||
)}
|
||||
</TorrentListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user