mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
Merge pull request #63 from YouROK/changes-in-cache/details
Changes in cache/details. Torrent cards rewriting
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"react/prop-types": 0,
|
||||
"react/react-in-jsx-scope": 0,
|
||||
"react/jsx-uses-react": 0,
|
||||
"import/no-unresolved": 0 // used to allow relative paths from "src" folder
|
||||
"import/no-unresolved": 0, // used to allow relative paths from "src" folder
|
||||
"react/jsx-props-no-spreading": 0
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,19 @@
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"axios": "^0.21.1",
|
||||
"clsx": "^1.1.1",
|
||||
"fontsource-roboto": "^4.0.0",
|
||||
"konva": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"material-ui-image": "^3.3.2",
|
||||
"parse-torrent-title": "^1.3.0",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-div-100vh": "^0.6.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-konva": "^17.0.2-4",
|
||||
"react-measure": "^2.5.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"styled-components": "^5.3.0"
|
||||
},
|
||||
|
||||
@@ -10,44 +10,6 @@
|
||||
<title>TorrServer</title>
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
.cache {
|
||||
padding-left:6px;
|
||||
padding-right:2px;
|
||||
line-height:11px
|
||||
}
|
||||
.piece {
|
||||
width:12px;
|
||||
height:12px;
|
||||
background-color:#eef2f4;
|
||||
border:1px solid #eef2f4;
|
||||
display:inline-block;
|
||||
margin-right:1px
|
||||
}
|
||||
.piece-complete{
|
||||
background-color:#3fb57a;
|
||||
border-color:#3fb57a;
|
||||
}
|
||||
.piece-loading{
|
||||
background-color:#00d0d0;
|
||||
border-color:#00d0d0;
|
||||
}
|
||||
.reader-range{
|
||||
border-color: #9a9aff !important;
|
||||
}
|
||||
.piece-reader{
|
||||
border-color: #000000 !important;
|
||||
}
|
||||
.piece-progress{
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color:#3fb57a;
|
||||
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: 12px;
|
||||
}
|
||||
</style>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
|
||||
|
||||
import Appbar from './components/Appbar/index'
|
||||
|
||||
const baseTheme = createMuiTheme({
|
||||
overrides: {
|
||||
MuiCssBaseline: {
|
||||
'@global': {
|
||||
html: {
|
||||
WebkitFontSmoothing: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#3fb57a',
|
||||
},
|
||||
secondary: {
|
||||
main: '#FFA724',
|
||||
},
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
})
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<MuiThemeProvider theme={baseTheme}>
|
||||
<CssBaseline />
|
||||
<Appbar />
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
60
web/src/App/Sidebar.jsx
Normal file
60
web/src/App/Sidebar.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { playlistAllHost, shutdownHost } from 'utils/Hosts'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import AddDialogButton from 'components/Add'
|
||||
import RemoveAll from 'components/RemoveAll'
|
||||
import SettingsDialog from 'components/Settings'
|
||||
import AboutDialog from 'components/About'
|
||||
import UploadDialog from 'components/Upload'
|
||||
import {
|
||||
CreditCard as CreditCardIcon,
|
||||
List as ListIcon,
|
||||
PowerSettingsNew as PowerSettingsNewIcon,
|
||||
} from '@material-ui/icons'
|
||||
import List from '@material-ui/core/List'
|
||||
|
||||
import { AppSidebarStyle } from './style'
|
||||
|
||||
export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) {
|
||||
return (
|
||||
<AppSidebarStyle isDrawerOpen={isDrawerOpen}>
|
||||
<List>
|
||||
<AddDialogButton />
|
||||
<UploadDialog />
|
||||
<RemoveAll />
|
||||
<ListItem button component='a' key='Playlist all torrents' target='_blank' href={playlistAllHost()}>
|
||||
<ListItemIcon>
|
||||
<ListIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Playlist all torrents' />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<SettingsDialog />
|
||||
<AboutDialog />
|
||||
<ListItem button key='Close server' onClick={() => fetch(shutdownHost())}>
|
||||
<ListItemIcon>
|
||||
<PowerSettingsNewIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Close server' />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<ListItem button key='Donation' onClick={() => setIsDonationDialogOpen(true)}>
|
||||
<ListItemIcon>
|
||||
<CreditCardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Donate' />
|
||||
</ListItem>
|
||||
</List>
|
||||
</AppSidebarStyle>
|
||||
)
|
||||
}
|
||||
63
web/src/App/index.jsx
Normal file
63
web/src/App/index.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { Menu as MenuIcon, Close as CloseIcon } from '@material-ui/icons'
|
||||
import { echoHost } from 'utils/Hosts'
|
||||
import TorrentList from 'components/TorrentList'
|
||||
import DonateSnackbar from 'components/Donate'
|
||||
import DonateDialog from 'components/Donate/DonateDialog'
|
||||
import Div100vh from 'react-div-100vh'
|
||||
import axios from 'axios'
|
||||
|
||||
import { AppWrapper, AppHeader } from './style'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
const baseTheme = createMuiTheme({
|
||||
overrides: { MuiCssBaseline: { '@global': { html: { WebkitFontSmoothing: 'auto' } } } },
|
||||
palette: { primary: { main: '#3fb57a' }, secondary: { main: '#FFA724' }, tonalOffset: 0.2 },
|
||||
})
|
||||
|
||||
export default function App() {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const [isDonationDialogOpen, setIsDonationDialogOpen] = useState(false)
|
||||
const [torrServerVersion, setTorrServerVersion] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MuiThemeProvider theme={baseTheme}>
|
||||
<CssBaseline />
|
||||
|
||||
{/* Div100vh - iOS WebKit fix */}
|
||||
<Div100vh>
|
||||
<AppWrapper>
|
||||
<AppHeader>
|
||||
<IconButton
|
||||
style={{ marginRight: '20px' }}
|
||||
color='inherit'
|
||||
onClick={() => setIsDrawerOpen(!isDrawerOpen)}
|
||||
edge='start'
|
||||
>
|
||||
{isDrawerOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
</IconButton>
|
||||
|
||||
<Typography variant='h6' noWrap>
|
||||
TorrServer {torrServerVersion}
|
||||
</Typography>
|
||||
</AppHeader>
|
||||
|
||||
<Sidebar isDrawerOpen={isDrawerOpen} setIsDonationDialogOpen={setIsDonationDialogOpen} />
|
||||
|
||||
<TorrentList />
|
||||
|
||||
{isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />}
|
||||
{!JSON.parse(localStorage.getItem('snackbarIsClosed')) && <DonateSnackbar />}
|
||||
</AppWrapper>
|
||||
</Div100vh>
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
64
web/src/App/style.js
Normal file
64
web/src/App/style.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
export const AppWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-rows: 60px 1fr;
|
||||
grid-template-areas:
|
||||
'head head'
|
||||
'side content';
|
||||
`
|
||||
|
||||
export const CenteredGrid = styled.div`
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
`
|
||||
|
||||
export const AppHeader = styled.div`
|
||||
background: #3fb57a;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
grid-area: head;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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%);
|
||||
padding: 0 24px;
|
||||
z-index: 3;
|
||||
`
|
||||
export const AppSidebarStyle = styled.div`
|
||||
${({ isDrawerOpen }) => css`
|
||||
grid-area: side;
|
||||
width: ${isDrawerOpen ? '400%' : '100%'};
|
||||
z-index: 2;
|
||||
overflow-x: hidden;
|
||||
transition: width 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
`}
|
||||
`
|
||||
export const TorrentListWrapper = styled.div`
|
||||
grid-area: content;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
|
||||
display: grid;
|
||||
place-content: start;
|
||||
grid-template-columns: repeat(auto-fit, minmax(max-content, 570px));
|
||||
gap: 20px;
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
padding: 10px;
|
||||
gap: 15px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
@@ -3,7 +3,6 @@ import Button from '@material-ui/core/Button'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import InfoIcon from '@material-ui/icons/Info'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
@@ -27,7 +26,6 @@ export default function AboutDialog() {
|
||||
|
||||
<DialogContent>
|
||||
<DialogContent>
|
||||
<DialogContentText id='alert-dialog-description'>
|
||||
<center>
|
||||
<h2>Thanks to everyone who tested and helped.</h2>
|
||||
</center>
|
||||
@@ -43,7 +41,6 @@ export default function AboutDialog() {
|
||||
<br />
|
||||
<b>SpAwN_LMG</b>
|
||||
<br />
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
</DialogContent>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function AddDialog({ handleClose }) {
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button disabled={!magnet} onClick={handleCloseSave} color='primary' variant='outlined'>
|
||||
<Button variant='contained' disabled={!magnet} onClick={handleCloseSave} color='primary'>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
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 Typography from '@material-ui/core/Typography'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import MenuIcon from '@material-ui/icons/Menu'
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import CreditCardIcon from '@material-ui/icons/CreditCard'
|
||||
import ListIcon from '@material-ui/icons/List'
|
||||
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
|
||||
import { playlistAllHost, shutdownHost, getTorrServerHost } from 'utils/Hosts'
|
||||
import TorrentList from 'components/TorrentList'
|
||||
import AddDialogButton from 'components/Add'
|
||||
import RemoveAll from 'components/RemoveAll'
|
||||
import SettingsDialog from 'components/Settings'
|
||||
import AboutDialog from 'components/About'
|
||||
import DonateSnackbar from 'components/Donate'
|
||||
import DonateDialog from 'components/Donate/DonateDialog'
|
||||
import UploadDialog from 'components/Upload'
|
||||
|
||||
import useStyles from './useStyles'
|
||||
|
||||
export default function MiniDrawer() {
|
||||
const classes = useStyles()
|
||||
const theme = useTheme()
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const [isDonationDialogOpen, setIsDonationDialogOpen] = useState(false)
|
||||
const [tsVersion, setTSVersion] = useState('')
|
||||
|
||||
const handleDrawerOpen = () => setIsDrawerOpen(true)
|
||||
const handleDrawerClose = () => setIsDrawerOpen(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${getTorrServerHost()}/echo`)
|
||||
.then(resp => resp.text())
|
||||
.then(txt => {
|
||||
if (!txt.startsWith('<!DOCTYPE html>')) setTSVersion(txt)
|
||||
})
|
||||
}, [isDrawerOpen])
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar
|
||||
position='fixed'
|
||||
className={clsx(classes.appBar, {
|
||||
[classes.appBarShift]: isDrawerOpen,
|
||||
})}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
aria-label='open drawer'
|
||||
onClick={handleDrawerOpen}
|
||||
edge='start'
|
||||
className={clsx(classes.menuButton, {
|
||||
[classes.hide]: isDrawerOpen,
|
||||
})}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant='h6' noWrap>
|
||||
TorrServer {tsVersion}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant='permanent'
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerOpen]: isDrawerOpen,
|
||||
[classes.drawerClose]: !isDrawerOpen,
|
||||
})}
|
||||
classes={{
|
||||
paper: clsx({
|
||||
[classes.drawerOpen]: isDrawerOpen,
|
||||
[classes.drawerClose]: !isDrawerOpen,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div className={classes.toolbar}>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<AddDialogButton />
|
||||
<UploadDialog />
|
||||
<RemoveAll />
|
||||
<ListItem button component='a' key='Playlist all torrents' target='_blank' href={playlistAllHost()}>
|
||||
<ListItemIcon>
|
||||
<ListIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Playlist all torrents' />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<SettingsDialog />
|
||||
<AboutDialog />
|
||||
<ListItem button key='Close server' onClick={() => fetch(shutdownHost())}>
|
||||
<ListItemIcon>
|
||||
<PowerSettingsNewIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Close server' />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<ListItem button key='Donation' onClick={() => setIsDonationDialogOpen(true)}>
|
||||
<ListItemIcon>
|
||||
<CreditCardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Donate' />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Drawer>
|
||||
|
||||
<main className={classes.content}>
|
||||
<div className={classes.toolbar} />
|
||||
|
||||
<TorrentList />
|
||||
</main>
|
||||
|
||||
{isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />}
|
||||
{!JSON.parse(localStorage.getItem('snackbarIsClosed')) && <DonateSnackbar />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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: 1,
|
||||
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),
|
||||
},
|
||||
}))
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import { getPeerString, humanizeSize } from 'utils/Utils'
|
||||
import { cacheHost } from 'utils/Hosts'
|
||||
|
||||
export default function DialogCacheInfo({ hash, open }) {
|
||||
const [cache, setCache] = useState({})
|
||||
const timerID = useRef(-1)
|
||||
const [pMap, setPMap] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (hash)
|
||||
timerID.current = setInterval(() => {
|
||||
getCache(hash, cache => {
|
||||
setCache(cache)
|
||||
})
|
||||
}, 100)
|
||||
else clearInterval(timerID.current)
|
||||
|
||||
return () => {
|
||||
clearInterval(timerID.current)
|
||||
}
|
||||
}, [hash, open])
|
||||
|
||||
useEffect(() => {
|
||||
if (cache && cache.PiecesCount && cache.Pieces) {
|
||||
const map = []
|
||||
for (let i = 0; i < cache.PiecesCount; i++) {
|
||||
const reader = 0
|
||||
let cls = 'piece'
|
||||
let prc = 0
|
||||
if (cache.Pieces[i]) {
|
||||
if (cache.Pieces[i].Completed && cache.Pieces[i].Size >= cache.Pieces[i].Length) cls += ' piece-complete'
|
||||
else cls += ' piece-loading'
|
||||
prc = ((cache.Pieces[i].Size / cache.Pieces[i].Length) * 100).toFixed(2)
|
||||
}
|
||||
|
||||
cache.Readers.forEach(r => {
|
||||
if (i >= r.Start && i <= r.End && i !== r.Reader) cls += ' reader-range'
|
||||
if (i === r.Reader) {
|
||||
cls += ' piece-reader'
|
||||
}
|
||||
})
|
||||
map.push({
|
||||
prc,
|
||||
class: cls,
|
||||
info: i,
|
||||
reader,
|
||||
})
|
||||
}
|
||||
setPMap(map)
|
||||
}
|
||||
}, [cache.Pieces])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id='form-dialog-title'>
|
||||
<Typography>
|
||||
<b>Hash </b> {cache.Hash}
|
||||
<br />
|
||||
<b>Capacity </b> {humanizeSize(cache.Capacity)}
|
||||
<br />
|
||||
<b>Filled </b> {humanizeSize(cache.Filled)}
|
||||
<br />
|
||||
<b>Torrent size </b> {cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)}
|
||||
<br />
|
||||
<b>Pieces length </b> {humanizeSize(cache.PiecesLength)}
|
||||
<br />
|
||||
<b>Pieces count </b> {cache.PiecesCount}
|
||||
<br />
|
||||
<b>Peers: </b> {getPeerString(cache.Torrent)}
|
||||
<br />
|
||||
<b>Download speed </b>{' '}
|
||||
{cache.Torrent && cache.Torrent.download_speed ? `${humanizeSize(cache.Torrent.download_speed)}/sec` : ''}
|
||||
<br />
|
||||
<b>Upload speed </b>{' '}
|
||||
{cache.Torrent && cache.Torrent.upload_speed ? `${humanizeSize(cache.Torrent.upload_speed)}/sec` : ''}
|
||||
<br />
|
||||
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<div className='cache'>
|
||||
{pMap.map(itm => (
|
||||
<span key={itm.info} className={itm.class} title={itm.info}>
|
||||
{itm.prc > 0 && itm.prc < 100 && (
|
||||
<div className='piece-progress' style={{ height: `${(itm.prc / 100) * 12}px` }} />
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getCache(hash, callback) {
|
||||
try {
|
||||
fetch(cacheHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'get', hash }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
json => {
|
||||
callback(json)
|
||||
},
|
||||
error => {
|
||||
callback({})
|
||||
console.error(error)
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
callback({})
|
||||
}
|
||||
}
|
||||
/*
|
||||
{
|
||||
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
|
||||
"Capacity": 209715200,
|
||||
"Filled": 2914808,
|
||||
"PiecesLength": 4194304,
|
||||
"PiecesCount": 2065,
|
||||
"DownloadSpeed": 32770.860273455524,
|
||||
"Pieces": {
|
||||
"2064": {
|
||||
"Id": 2064,
|
||||
"Length": 2914808,
|
||||
"Size": 162296,
|
||||
"Completed": false
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SectionTitle, WidgetWrapper } from '../style'
|
||||
import { DetailedViewCacheSection, DetailedViewWidgetSection } from './style'
|
||||
import TorrentCache from '../TorrentCache'
|
||||
import {
|
||||
SizeWidget,
|
||||
PiecesLengthWidget,
|
||||
StatusWidget,
|
||||
PiecesCountWidget,
|
||||
PeersWidget,
|
||||
UploadSpeedWidget,
|
||||
DownlodSpeedWidget,
|
||||
} from '../widgets'
|
||||
|
||||
export default function Test({
|
||||
downloadSpeed,
|
||||
uploadSpeed,
|
||||
torrent,
|
||||
torrentSize,
|
||||
PiecesCount,
|
||||
PiecesLength,
|
||||
statString,
|
||||
cache,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DetailedViewWidgetSection>
|
||||
<SectionTitle mb={20}>Data</SectionTitle>
|
||||
<WidgetWrapper detailedView>
|
||||
<DownlodSpeedWidget data={downloadSpeed} />
|
||||
<UploadSpeedWidget data={uploadSpeed} />
|
||||
<PeersWidget data={torrent} />
|
||||
<SizeWidget data={torrentSize} />
|
||||
<PiecesCountWidget data={PiecesCount} />
|
||||
<PiecesLengthWidget data={PiecesLength} />
|
||||
<StatusWidget data={statString} />
|
||||
</WidgetWrapper>
|
||||
</DetailedViewWidgetSection>
|
||||
|
||||
<DetailedViewCacheSection>
|
||||
<SectionTitle mb={20}>Cache</SectionTitle>
|
||||
<TorrentCache cache={cache} />
|
||||
</DetailedViewCacheSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const DetailedViewWidgetSection = styled.section`
|
||||
padding: 40px;
|
||||
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const DetailedViewCacheSection = styled.section`
|
||||
padding: 40px;
|
||||
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,33 @@
|
||||
import Button from '@material-ui/core/Button'
|
||||
import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core'
|
||||
import CloseIcon from '@material-ui/icons/Close'
|
||||
import { ArrowBack } from '@material-ui/icons'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
appBar: { position: 'relative' },
|
||||
title: { marginLeft: theme.spacing(2), flex: 1 },
|
||||
}))
|
||||
|
||||
export default function DialogHeader({ title, onClose, onBack }) {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<AppBar className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton edge='start' color='inherit' onClick={onBack || onClose} aria-label='close'>
|
||||
{onBack ? <ArrowBack /> : <CloseIcon />}
|
||||
</IconButton>
|
||||
|
||||
<Typography variant='h6' className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{onBack && (
|
||||
<Button autoFocus color='inherit' onClick={onClose}>
|
||||
close
|
||||
</Button>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Rect } from 'react-konva'
|
||||
|
||||
export default function SingleBlock({
|
||||
x,
|
||||
y,
|
||||
percentage,
|
||||
isActive = false,
|
||||
inProgress = false,
|
||||
isReaderRange = false,
|
||||
isComplete = false,
|
||||
boxHeight,
|
||||
strokeWidth,
|
||||
}) {
|
||||
const strokeColor = isActive
|
||||
? '#000'
|
||||
: isComplete
|
||||
? '#3fb57a'
|
||||
: inProgress
|
||||
? '#00d0d0'
|
||||
: isReaderRange
|
||||
? '#9a9aff'
|
||||
: '#eef2f4'
|
||||
const backgroundColor = inProgress ? '#00d0d0' : '#eef2f4'
|
||||
const percentageProgressColor = '#3fb57a'
|
||||
const processCompletedColor = '#3fb57a'
|
||||
|
||||
return (
|
||||
<Rect
|
||||
x={x}
|
||||
y={y}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
height={boxHeight}
|
||||
width={boxHeight}
|
||||
fillAfterStrokeEnabled
|
||||
preventDefault={false}
|
||||
{...(isComplete
|
||||
? { fill: processCompletedColor }
|
||||
: inProgress && {
|
||||
fillLinearGradientStartPointY: boxHeight,
|
||||
fillLinearGradientEndPointY: 0,
|
||||
fillLinearGradientColorStops: [
|
||||
0,
|
||||
percentageProgressColor,
|
||||
percentage,
|
||||
percentageProgressColor,
|
||||
percentage,
|
||||
backgroundColor,
|
||||
],
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { WidgetFieldWrapper, WidgetFieldIcon, WidgetFieldValue, WidgetFieldTitle } from './style'
|
||||
|
||||
export default function StatisticsField({ icon: Icon, title, value, iconBg, valueBg }) {
|
||||
return (
|
||||
<WidgetFieldWrapper>
|
||||
<WidgetFieldTitle>{title}</WidgetFieldTitle>
|
||||
<WidgetFieldIcon bgColor={iconBg}>
|
||||
<Icon />
|
||||
</WidgetFieldIcon>
|
||||
|
||||
<WidgetFieldValue bgColor={valueBg}>{value}</WidgetFieldValue>
|
||||
</WidgetFieldWrapper>
|
||||
)
|
||||
}
|
||||
147
web/src/components/DialogTorrentDetailsContent/Table/index.jsx
Normal file
147
web/src/components/DialogTorrentDetailsContent/Table/index.jsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { streamHost } from 'utils/Hosts'
|
||||
import { isEqual } from 'lodash'
|
||||
import { humanizeSize } from 'utils/Utils'
|
||||
import ptt from 'parse-torrent-title'
|
||||
import { Button } from '@material-ui/core'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
|
||||
import { TableStyle, ShortTableWrapper, ShortTable } from './style'
|
||||
|
||||
const { memo } = require('react')
|
||||
|
||||
const Table = memo(
|
||||
({ playableFileList, viewedFileList, selectedSeason, seasonAmount, hash }) => {
|
||||
const preloadBuffer = fileId => fetch(`${streamHost()}?link=${hash}&index=${fileId}&preload`)
|
||||
const getFileLink = (path, id) =>
|
||||
`${streamHost()}/${encodeURIComponent(path.split('\\').pop().split('/').pop())}?link=${hash}&index=${id}&play`
|
||||
const fileHasEpisodeText = !!playableFileList?.find(({ path }) => ptt.parse(path).episode)
|
||||
const fileHasSeasonText = !!playableFileList?.find(({ path }) => ptt.parse(path).season)
|
||||
const fileHasResolutionText = !!playableFileList?.find(({ path }) => ptt.parse(path).resolution)
|
||||
|
||||
return !playableFileList?.length ? (
|
||||
'No playable files in this torrent'
|
||||
) : (
|
||||
<>
|
||||
<TableStyle>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '0' }}>viewed</th>
|
||||
<th>name</th>
|
||||
{fileHasSeasonText && seasonAmount?.length === 1 && <th style={{ width: '0' }}>season</th>}
|
||||
{fileHasEpisodeText && <th style={{ width: '0' }}>episode</th>}
|
||||
{fileHasResolutionText && <th style={{ width: '0' }}>resolution</th>}
|
||||
<th style={{ width: '100px' }}>size</th>
|
||||
<th style={{ width: '400px' }}>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{playableFileList.map(({ id, path, length }) => {
|
||||
const { title, resolution, episode, season } = ptt.parse(path)
|
||||
const isViewed = viewedFileList?.includes(id)
|
||||
const link = getFileLink(path, id)
|
||||
|
||||
return (
|
||||
(season === selectedSeason || !seasonAmount?.length) && (
|
||||
<tr key={id} className={isViewed ? 'viewed-file-row' : null}>
|
||||
<td data-label='viewed' className={isViewed ? 'viewed-file-indicator' : null} />
|
||||
<td data-label='name'>{title}</td>
|
||||
{fileHasSeasonText && seasonAmount?.length === 1 && <td data-label='season'>{season}</td>}
|
||||
{fileHasEpisodeText && <td data-label='episode'>{episode}</td>}
|
||||
{fileHasResolutionText && <td data-label='resolution'>{resolution}</td>}
|
||||
<td data-label='size'>{humanizeSize(length)}</td>
|
||||
<td className='button-cell'>
|
||||
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
|
||||
Preload
|
||||
</Button>
|
||||
|
||||
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
|
||||
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
|
||||
Open link
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<CopyToClipboard text={link}>
|
||||
<Button variant='outlined' color='primary' size='small'>
|
||||
Copy link
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
|
||||
<ShortTableWrapper>
|
||||
{playableFileList.map(({ id, path, length }) => {
|
||||
const { title, resolution, episode, season } = ptt.parse(path)
|
||||
const isViewed = viewedFileList?.includes(id)
|
||||
const link = getFileLink(path, id)
|
||||
|
||||
return (
|
||||
(season === selectedSeason || !seasonAmount?.length) && (
|
||||
<ShortTable key={id} isViewed={isViewed}>
|
||||
<div className='short-table-name'>{title}</div>
|
||||
<div className='short-table-data'>
|
||||
{isViewed && (
|
||||
<div className='short-table-field'>
|
||||
<div className='short-table-field-name'>viewed</div>
|
||||
<div className='short-table-field-value'>
|
||||
<div className='short-table-viewed-indicator' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{fileHasSeasonText && seasonAmount?.length === 1 && (
|
||||
<div className='short-table-field'>
|
||||
<div className='short-table-field-name'>season</div>
|
||||
<div className='short-table-field-value'>{season}</div>
|
||||
</div>
|
||||
)}
|
||||
{fileHasEpisodeText && (
|
||||
<div className='short-table-field'>
|
||||
<div className='short-table-field-name'>epoisode</div>
|
||||
<div className='short-table-field-value'>{episode}</div>
|
||||
</div>
|
||||
)}
|
||||
{fileHasResolutionText && (
|
||||
<div className='short-table-field'>
|
||||
<div className='short-table-field-name'>resolution</div>
|
||||
<div className='short-table-field-value'>{resolution}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='short-table-field'>
|
||||
<div className='short-table-field-name'>size</div>
|
||||
<div className='short-table-field-value'>{humanizeSize(length)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='short-table-buttons'>
|
||||
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
|
||||
Preload
|
||||
</Button>
|
||||
|
||||
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
|
||||
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
|
||||
Open link
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<CopyToClipboard text={link}>
|
||||
<Button variant='outlined' color='primary' size='small'>
|
||||
Copy link
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</ShortTable>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</ShortTableWrapper>
|
||||
</>
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev, next),
|
||||
)
|
||||
|
||||
export default Table
|
||||
172
web/src/components/DialogTorrentDetailsContent/Table/style.js
Normal file
172
web/src/components/DialogTorrentDetailsContent/Table/style.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
const viewedIndicator = css`
|
||||
:before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #15d5af;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
`
|
||||
export const TableStyle = styled.table`
|
||||
border-collapse: collapse;
|
||||
margin: 25px 0;
|
||||
font-size: 0.9em;
|
||||
width: 100%;
|
||||
border-radius: 5px 5px 0 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
thead tr {
|
||||
background: #009879;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
}
|
||||
|
||||
&.viewed-file-row {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.viewed-file-indicator {
|
||||
position: relative;
|
||||
|
||||
${viewedIndicator}
|
||||
}
|
||||
|
||||
&.button-cell {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 970px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const ShortTableWrapper = styled.div`
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
display: none;
|
||||
|
||||
@media (max-width: 970px) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
gap: 15px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
export const ShortTable = styled.div`
|
||||
${({ isViewed }) => css`
|
||||
width: 100%;
|
||||
grid-template-rows: repeat(3, max-content);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.short-table {
|
||||
&-name {
|
||||
background: ${isViewed ? '#bdbdbd' : '#009879'};
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
|
||||
@media (max-width: 880px) {
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
&-data {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: ${isViewed ? 'max-content' : '1fr'};
|
||||
grid-auto-columns: 1fr;
|
||||
}
|
||||
&-field {
|
||||
display: grid;
|
||||
grid-template-rows: 30px 1fr;
|
||||
background: black;
|
||||
:not(:last-child) {
|
||||
border-right: 1px solid ${isViewed ? '#bdbdbd' : '#019376'};
|
||||
}
|
||||
|
||||
&-name {
|
||||
background: ${isViewed ? '#c4c4c4' : '#00a383'};
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0 10px;
|
||||
|
||||
@media (max-width: 880px) {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
background: ${isViewed ? '#c9c9c9' : '#03aa89'};
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
padding: 15px 10px;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 880px) {
|
||||
font-size: 13px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-viewed-indicator {
|
||||
${isViewed && viewedIndicator}
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
padding: 20px;
|
||||
border-bottom: 2px solid ${isViewed ? '#bdbdbd' : '#009879'};
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
@media (max-width: 410px) {
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`
|
||||
110
web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx
Normal file
110
web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import { Stage, Layer } from 'react-konva'
|
||||
import Measure from 'react-measure'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import SingleBlock from './SingleBlock'
|
||||
import { useCreateCacheMap } from './customHooks'
|
||||
|
||||
const TorrentCache = memo(
|
||||
({ cache, isMini }) => {
|
||||
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
|
||||
const [stageSettings, setStageSettings] = useState({
|
||||
boxHeight: null,
|
||||
strokeWidth: null,
|
||||
marginBetweenBlocks: null,
|
||||
stageOffset: null,
|
||||
})
|
||||
|
||||
const cacheMap = useCreateCacheMap(cache)
|
||||
|
||||
const updateStageSettings = (boxHeight, strokeWidth) => {
|
||||
setStageSettings({
|
||||
boxHeight,
|
||||
strokeWidth,
|
||||
marginBetweenBlocks: strokeWidth,
|
||||
stageOffset: strokeWidth * 2,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// initializing stageSettings
|
||||
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
|
||||
updateStageSettings(12, 2)
|
||||
}, [isMini, dimensions.width])
|
||||
|
||||
const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings
|
||||
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
|
||||
const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks
|
||||
const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin)
|
||||
const amountOfBlocksToRenderInShortView =
|
||||
preloadPiecesAmount === piecesInOneRow
|
||||
? preloadPiecesAmount - 1
|
||||
: preloadPiecesAmount + piecesInOneRow - (preloadPiecesAmount % piecesInOneRow) - 1
|
||||
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
|
||||
let activeId = null
|
||||
|
||||
return (
|
||||
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
||||
{({ measureRef }) => (
|
||||
<div ref={measureRef}>
|
||||
<DialogContent style={{ padding: 0 }}>
|
||||
<Stage
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
offset={{ x: -stageOffset, y: -stageOffset }}
|
||||
width={stageOffset + blockSizeWithMargin * piecesInOneRow || 0}
|
||||
height={stageOffset + blockSizeWithMargin * amountOfRows || 0}
|
||||
>
|
||||
<Layer>
|
||||
{cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
|
||||
const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow)
|
||||
|
||||
// -------- related only for short view -------
|
||||
if (isActive) activeId = id
|
||||
const shouldBeRendered =
|
||||
isActive || (id - activeId <= amountOfBlocksToRenderInShortView && id - activeId >= 0)
|
||||
// --------------------------------------------
|
||||
|
||||
return isMini ? (
|
||||
shouldBeRendered && (
|
||||
<SingleBlock
|
||||
key={id}
|
||||
x={((id - activeId) % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<SingleBlock
|
||||
key={id}
|
||||
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
|
||||
)
|
||||
|
||||
export default TorrentCache
|
||||
@@ -0,0 +1,83 @@
|
||||
import axios from 'axios'
|
||||
import { memo } from 'react'
|
||||
import { playlistTorrHost, torrentsHost, viewedHost } from 'utils/Hosts'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
import { Button } from '@material-ui/core'
|
||||
import ptt from 'parse-torrent-title'
|
||||
|
||||
import { SmallLabel, MainSectionButtonGroup } from './style'
|
||||
import { SectionSubName } from '../style'
|
||||
|
||||
const TorrentFunctions = memo(
|
||||
({ hash, viewedFileList, playableFileList, name, title, setViewedFileList }) => {
|
||||
const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1]
|
||||
const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path
|
||||
const isOnlyOnePlayableFile = playableFileList?.length === 1
|
||||
const latestViewedFileData = latestViewedFile && ptt.parse(latestViewedFile)
|
||||
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
|
||||
const removeTorrentViews = () =>
|
||||
axios.post(viewedHost(), { action: 'rem', hash, file_index: -1 }).then(() => setViewedFileList())
|
||||
const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(name || title || 'file')}.m3u?link=${hash}&m3u`
|
||||
const partialPlaylistLink = `${fullPlaylistLink}&fromlast`
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOnlyOnePlayableFile && !!viewedFileList?.length && (
|
||||
<>
|
||||
<SmallLabel>Download Playlist</SmallLabel>
|
||||
<SectionSubName mb={10}>
|
||||
<strong>Latest file played:</strong> {latestViewedFileData?.title}.
|
||||
{latestViewedFileData?.season && (
|
||||
<>
|
||||
{' '}
|
||||
Season: {latestViewedFileData?.season}. Episode: {latestViewedFileData?.episode}.
|
||||
</>
|
||||
)}
|
||||
</SectionSubName>
|
||||
|
||||
<MainSectionButtonGroup>
|
||||
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||
full
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a style={{ textDecoration: 'none' }} href={partialPlaylistLink}>
|
||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||
from latest file
|
||||
</Button>
|
||||
</a>
|
||||
</MainSectionButtonGroup>
|
||||
</>
|
||||
)}
|
||||
<SmallLabel mb={10}>Torrent State</SmallLabel>
|
||||
<MainSectionButtonGroup>
|
||||
<Button onClick={() => removeTorrentViews()} variant='contained' color='primary' size='large'>
|
||||
remove views
|
||||
</Button>
|
||||
<Button onClick={() => dropTorrent()} variant='contained' color='primary' size='large'>
|
||||
drop torrent
|
||||
</Button>
|
||||
</MainSectionButtonGroup>
|
||||
<SmallLabel mb={10}>Info</SmallLabel>
|
||||
<MainSectionButtonGroup>
|
||||
{(isOnlyOnePlayableFile || !viewedFileList?.length) && (
|
||||
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||
download playlist
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
<CopyToClipboard text={hash}>
|
||||
<Button variant='contained' color='primary' size='large'>
|
||||
copy hash
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</MainSectionButtonGroup>
|
||||
</>
|
||||
)
|
||||
},
|
||||
() => true,
|
||||
)
|
||||
|
||||
export default TorrentFunctions
|
||||
@@ -0,0 +1,33 @@
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
export const MainSectionButtonGroup = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
:not(:last-child) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 1580px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
export const SmallLabel = styled.div`
|
||||
${({ mb }) => css`
|
||||
${mb && `margin-bottom: ${mb}px`};
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 18px;
|
||||
${mb && `margin-bottom: ${mb / 1.5}px`};
|
||||
}
|
||||
`}
|
||||
`
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { cacheHost, settingsHost } from 'utils/Hosts'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useUpdateCache = hash => {
|
||||
const [cache, setCache] = useState({})
|
||||
const componentIsMounted = useRef(true)
|
||||
const timerID = useRef(null)
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// this function is required to notify "updateCache" when NOT to make state update
|
||||
componentIsMounted.current = false
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hash) {
|
||||
timerID.current = setInterval(() => {
|
||||
const updateCache = newCache => componentIsMounted.current && setCache(newCache)
|
||||
|
||||
axios
|
||||
.post(cacheHost(), { action: 'get', hash })
|
||||
.then(({ data }) => updateCache(data))
|
||||
// empty cache if error
|
||||
.catch(() => updateCache({}))
|
||||
}, 100)
|
||||
} else clearInterval(timerID.current)
|
||||
|
||||
return () => clearInterval(timerID.current)
|
||||
}, [hash])
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
export const useCreateCacheMap = cache => {
|
||||
const [cacheMap, setCacheMap] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!cache.PiecesCount || !cache.Pieces) return
|
||||
|
||||
const { Pieces, PiecesCount, Readers } = cache
|
||||
|
||||
const map = []
|
||||
|
||||
for (let i = 0; i < PiecesCount; i++) {
|
||||
const newPiece = { id: i }
|
||||
|
||||
const currentPiece = Pieces[i]
|
||||
if (currentPiece) {
|
||||
if (currentPiece.Completed && currentPiece.Size === currentPiece.Length) newPiece.isComplete = true
|
||||
else {
|
||||
newPiece.inProgress = true
|
||||
newPiece.percentage = (currentPiece.Size / currentPiece.Length).toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
Readers.forEach(r => {
|
||||
if (i === r.Reader) newPiece.isActive = true
|
||||
if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true
|
||||
})
|
||||
|
||||
map.push(newPiece)
|
||||
}
|
||||
|
||||
setCacheMap(map)
|
||||
}, [cache])
|
||||
|
||||
return cacheMap
|
||||
}
|
||||
|
||||
export const useGetSettings = cache => {
|
||||
const [settings, setSettings] = useState()
|
||||
useEffect(() => {
|
||||
axios.post(settingsHost(), { action: 'get' }).then(({ data }) => setSettings(data))
|
||||
}, [cache])
|
||||
|
||||
return settings
|
||||
}
|
||||
68
web/src/components/DialogTorrentDetailsContent/helpers.js
Normal file
68
web/src/components/DialogTorrentDetailsContent/helpers.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const getExt = filename => {
|
||||
const ext = filename.split('.').pop()
|
||||
if (ext === filename) return ''
|
||||
return ext.toLowerCase()
|
||||
}
|
||||
const playableExtList = [
|
||||
// video
|
||||
'3g2',
|
||||
'3gp',
|
||||
'aaf',
|
||||
'asf',
|
||||
'avchd',
|
||||
'avi',
|
||||
'drc',
|
||||
'flv',
|
||||
'iso',
|
||||
'm2v',
|
||||
'm2ts',
|
||||
'm4p',
|
||||
'm4v',
|
||||
'mkv',
|
||||
'mng',
|
||||
'mov',
|
||||
'mp2',
|
||||
'mp4',
|
||||
'mpe',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'mpv',
|
||||
'mxf',
|
||||
'nsv',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'ts',
|
||||
'qt',
|
||||
'rm',
|
||||
'rmvb',
|
||||
'roq',
|
||||
'svi',
|
||||
'vob',
|
||||
'webm',
|
||||
'wmv',
|
||||
'yuv',
|
||||
// audio
|
||||
'aac',
|
||||
'aiff',
|
||||
'ape',
|
||||
'au',
|
||||
'flac',
|
||||
'gsm',
|
||||
'it',
|
||||
'm3u',
|
||||
'm4a',
|
||||
'mid',
|
||||
'mod',
|
||||
'mp3',
|
||||
'mpa',
|
||||
'pls',
|
||||
'ra',
|
||||
's3m',
|
||||
'sid',
|
||||
'wav',
|
||||
'wma',
|
||||
'xm',
|
||||
]
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const isFilePlayable = fileName => playableExtList.includes(getExt(fileName))
|
||||
231
web/src/components/DialogTorrentDetailsContent/index.jsx
Normal file
231
web/src/components/DialogTorrentDetailsContent/index.jsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { NoImageIcon } from 'icons'
|
||||
import { humanizeSize, shortenText } from 'utils/Utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, ButtonGroup } from '@material-ui/core'
|
||||
import ptt from 'parse-torrent-title'
|
||||
import axios from 'axios'
|
||||
import { viewedHost } from 'utils/Hosts'
|
||||
import { GETTING_INFO, IN_DB } from 'torrentStates'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
|
||||
import { useUpdateCache, useGetSettings } from './customHooks'
|
||||
import DialogHeader from './DialogHeader'
|
||||
import TorrentCache from './TorrentCache'
|
||||
import Table from './Table'
|
||||
import DetailedView from './DetailedView'
|
||||
import {
|
||||
DialogContentGrid,
|
||||
MainSection,
|
||||
Poster,
|
||||
SectionTitle,
|
||||
SectionSubName,
|
||||
WidgetWrapper,
|
||||
LoadingProgress,
|
||||
SectionHeader,
|
||||
CacheSection,
|
||||
TorrentFilesSection,
|
||||
Divider,
|
||||
} from './style'
|
||||
import { DownlodSpeedWidget, UploadSpeedWidget, PeersWidget, SizeWidget } from './widgets'
|
||||
import TorrentFunctions from './TorrentFunctions'
|
||||
import { isFilePlayable } from './helpers'
|
||||
|
||||
const Loader = () => (
|
||||
<div style={{ minHeight: '80vh', display: 'grid', placeItems: 'center' }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isDetailedCacheView, setIsDetailedCacheView] = useState(false)
|
||||
const [viewedFileList, setViewedFileList] = useState()
|
||||
const [playableFileList, setPlayableFileList] = useState()
|
||||
const [seasonAmount, setSeasonAmount] = useState(null)
|
||||
const [selectedSeason, setSelectedSeason] = useState()
|
||||
|
||||
const {
|
||||
poster,
|
||||
hash,
|
||||
title,
|
||||
name,
|
||||
stat,
|
||||
download_speed: downloadSpeed,
|
||||
upload_speed: uploadSpeed,
|
||||
stat_string: statString,
|
||||
torrent_size: torrentSize,
|
||||
file_stats: torrentFileList,
|
||||
} = torrent
|
||||
|
||||
const cache = useUpdateCache(hash)
|
||||
const settings = useGetSettings(cache)
|
||||
|
||||
const { Capacity, PiecesCount, PiecesLength, Filled } = cache
|
||||
|
||||
useEffect(() => {
|
||||
if (playableFileList && seasonAmount === null) {
|
||||
const seasons = []
|
||||
playableFileList.forEach(({ path }) => {
|
||||
const currentSeason = ptt.parse(path).season
|
||||
if (currentSeason) {
|
||||
!seasons.includes(currentSeason) && seasons.push(currentSeason)
|
||||
}
|
||||
})
|
||||
seasons.length && setSelectedSeason(seasons[0])
|
||||
setSeasonAmount(seasons.sort((a, b) => a - b))
|
||||
}
|
||||
}, [playableFileList, seasonAmount])
|
||||
|
||||
useEffect(() => {
|
||||
setPlayableFileList(torrentFileList?.filter(({ path }) => isFilePlayable(path)))
|
||||
}, [torrentFileList])
|
||||
|
||||
useEffect(() => {
|
||||
const cacheLoaded = !!Object.entries(cache).length
|
||||
const torrentLoaded = stat !== GETTING_INFO && stat !== IN_DB
|
||||
|
||||
if (!cacheLoaded && !isLoading) setIsLoading(true)
|
||||
if (cacheLoaded && isLoading && torrentLoaded) setIsLoading(false)
|
||||
}, [stat, cache, isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
// getting viewed file list
|
||||
axios.post(viewedHost(), { action: 'list', hash }).then(({ data }) => {
|
||||
if (data) {
|
||||
const lst = data.map(itm => itm.file_index).sort((a, b) => a - b)
|
||||
setViewedFileList(lst)
|
||||
} else setViewedFileList()
|
||||
})
|
||||
}, [hash])
|
||||
|
||||
const bufferSize = settings?.PreloadBuffer ? Capacity : 33554432 // Default is 32mb if PreloadBuffer is false
|
||||
|
||||
const getTitle = value => {
|
||||
const torrentParsedName = value && ptt.parse(value)
|
||||
const newNameStrings = []
|
||||
|
||||
if (torrentParsedName?.title) newNameStrings.push(` ${torrentParsedName?.title}`)
|
||||
if (torrentParsedName?.year) newNameStrings.push(`. ${torrentParsedName?.year}.`)
|
||||
if (torrentParsedName?.resolution) newNameStrings.push(` (${torrentParsedName?.resolution})`)
|
||||
|
||||
return newNameStrings.join(' ')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader
|
||||
onClose={closeDialog}
|
||||
title={isDetailedCacheView ? 'Detailed Cache View' : 'Torrent Details'}
|
||||
{...(isDetailedCacheView && { onBack: () => setIsDetailedCacheView(false) })}
|
||||
/>
|
||||
|
||||
<div style={{ minHeight: '80vh', overflow: 'auto' }}>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : isDetailedCacheView ? (
|
||||
<DetailedView
|
||||
downloadSpeed={downloadSpeed}
|
||||
uploadSpeed={uploadSpeed}
|
||||
torrent={torrent}
|
||||
torrentSize={torrentSize}
|
||||
PiecesCount={PiecesCount}
|
||||
PiecesLength={PiecesLength}
|
||||
statString={statString}
|
||||
cache={cache}
|
||||
/>
|
||||
) : (
|
||||
<DialogContentGrid>
|
||||
<MainSection>
|
||||
<Poster poster={poster}>{poster ? <img alt='poster' src={poster} /> : <NoImageIcon />}</Poster>
|
||||
|
||||
<div>
|
||||
{name && name !== title ? (
|
||||
<>
|
||||
<SectionTitle>{shortenText(getTitle(name), 50)}</SectionTitle>
|
||||
<SectionSubName mb={20}>{shortenText(title, 160)}</SectionSubName>
|
||||
</>
|
||||
) : (
|
||||
<SectionTitle mb={20}>{shortenText(getTitle(title), 50)}</SectionTitle>
|
||||
)}
|
||||
|
||||
<WidgetWrapper>
|
||||
<DownlodSpeedWidget data={downloadSpeed} />
|
||||
<UploadSpeedWidget data={uploadSpeed} />
|
||||
<PeersWidget data={torrent} />
|
||||
<SizeWidget data={torrentSize} />
|
||||
</WidgetWrapper>
|
||||
|
||||
<Divider />
|
||||
|
||||
<TorrentFunctions
|
||||
hash={hash}
|
||||
viewedFileList={viewedFileList}
|
||||
playableFileList={playableFileList}
|
||||
name={name}
|
||||
title={title}
|
||||
setViewedFileList={setViewedFileList}
|
||||
/>
|
||||
</div>
|
||||
</MainSection>
|
||||
|
||||
<CacheSection>
|
||||
<SectionHeader>
|
||||
<SectionTitle mb={20}>Buffer</SectionTitle>
|
||||
{!settings?.PreloadBuffer && (
|
||||
<SectionSubName>Enable "Preload Buffer" in settings to change buffer size</SectionSubName>
|
||||
)}
|
||||
<LoadingProgress
|
||||
value={Filled}
|
||||
fullAmount={bufferSize}
|
||||
label={`${humanizeSize(Filled) || '0 B'} / ${humanizeSize(bufferSize)}`}
|
||||
/>
|
||||
</SectionHeader>
|
||||
|
||||
<TorrentCache isMini cache={cache} />
|
||||
<Button
|
||||
style={{ marginTop: '30px' }}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='large'
|
||||
onClick={() => setIsDetailedCacheView(true)}
|
||||
>
|
||||
Detailed cache view
|
||||
</Button>
|
||||
</CacheSection>
|
||||
|
||||
<TorrentFilesSection>
|
||||
<SectionTitle mb={20}>Torrent Content</SectionTitle>
|
||||
|
||||
{seasonAmount?.length > 1 && (
|
||||
<>
|
||||
<SectionSubName mb={7}>Select Season</SectionSubName>
|
||||
<ButtonGroup style={{ marginBottom: '30px' }} color='primary'>
|
||||
{seasonAmount.map(season => (
|
||||
<Button
|
||||
key={season}
|
||||
variant={selectedSeason === season ? 'contained' : 'outlined'}
|
||||
onClick={() => setSelectedSeason(season)}
|
||||
>
|
||||
{season}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
<SectionTitle mb={20}>Season {selectedSeason}</SectionTitle>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Table
|
||||
hash={hash}
|
||||
playableFileList={playableFileList}
|
||||
viewedFileList={viewedFileList}
|
||||
selectedSeason={selectedSeason}
|
||||
seasonAmount={seasonAmount}
|
||||
/>
|
||||
</TorrentFilesSection>
|
||||
</DialogContentGrid>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
253
web/src/components/DialogTorrentDetailsContent/style.js
Normal file
253
web/src/components/DialogTorrentDetailsContent/style.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
export const DialogContentGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 70% 1fr;
|
||||
grid-template-rows: repeat(2, min-content);
|
||||
grid-template-areas:
|
||||
'main cache'
|
||||
'file-list file-list';
|
||||
|
||||
@media (max-width: 1450px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(3, min-content);
|
||||
grid-template-areas:
|
||||
'main'
|
||||
'cache'
|
||||
'file-list';
|
||||
}
|
||||
`
|
||||
export const Poster = styled.div`
|
||||
${({ poster }) => css`
|
||||
height: 400px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
|
||||
${poster
|
||||
? css`
|
||||
img {
|
||||
border-radius: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
width: 300px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #74c39c;
|
||||
|
||||
svg {
|
||||
transform: scale(2.5) translateY(-3px);
|
||||
}
|
||||
`}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
height: 200px;
|
||||
|
||||
${!poster &&
|
||||
css`
|
||||
width: 150px;
|
||||
svg {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
`}
|
||||
}
|
||||
`}
|
||||
`
|
||||
export const MainSection = styled.section`
|
||||
grid-area: main;
|
||||
padding: 40px;
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
gap: 30px;
|
||||
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
|
||||
|
||||
@media (max-width: 840px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const CacheSection = styled.section`
|
||||
grid-area: cache;
|
||||
padding: 40px;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
background: #88cdaa;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const TorrentFilesSection = styled.section`
|
||||
grid-area: file-list;
|
||||
padding: 40px;
|
||||
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const SectionSubName = styled.div`
|
||||
${({ mb }) => css`
|
||||
${mb && `margin-bottom: ${mb}px`};
|
||||
color: #7c7b7c;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
${mb && `margin-bottom: ${mb / 2}px`};
|
||||
font-size: 11px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const SectionTitle = styled.div`
|
||||
${({ mb }) => css`
|
||||
${mb && `margin-bottom: ${mb}px`};
|
||||
font-size: 35px;
|
||||
font-weight: 200;
|
||||
line-height: 1;
|
||||
word-break: break-word;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 25px;
|
||||
${mb && `margin-bottom: ${mb / 2}px`};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const SectionHeader = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export const WidgetWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(max-content, 220px));
|
||||
gap: 20px;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
gap: 15px;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
${({ detailedView }) =>
|
||||
detailedView
|
||||
? css`
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(max-content, 185px));
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 390px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const WidgetFieldWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
grid-template-rows: min-content 50px;
|
||||
grid-template-areas:
|
||||
'title title'
|
||||
'icon value';
|
||||
|
||||
> * {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: 30px 1fr;
|
||||
grid-template-rows: min-content 40px;
|
||||
}
|
||||
`
|
||||
export const WidgetFieldTitle = styled.div`
|
||||
grid-area: title;
|
||||
justify-self: start;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
margin-bottom: 2px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
export const WidgetFieldIcon = styled.div`
|
||||
${({ bgColor }) => css`
|
||||
grid-area: icon;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: ${bgColor};
|
||||
border-radius: 5px 0 0 5px;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
> svg {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`
|
||||
export const WidgetFieldValue = styled.div`
|
||||
${({ bgColor }) => css`
|
||||
grid-area: value;
|
||||
padding: 0 20px;
|
||||
color: #fff;
|
||||
font-size: 25px;
|
||||
background: ${bgColor};
|
||||
border-radius: 0 5px 5px 0;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 18px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const LoadingProgress = styled.div.attrs(({ value, fullAmount }) => {
|
||||
const percentage = Math.min(100, (value * 100) / fullAmount)
|
||||
|
||||
return {
|
||||
// this block is here according to styled-components recomendation about fast changable components
|
||||
style: {
|
||||
background: `linear-gradient(to right, #b5dec9 0%, #b5dec9 ${percentage}%, #fff ${percentage}%, #fff 100%)`,
|
||||
},
|
||||
}
|
||||
})`
|
||||
${({ label }) => css`
|
||||
border: 1px solid;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
|
||||
:before {
|
||||
content: '${label}';
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const Divider = styled.div`
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
margin: 30px 0;
|
||||
`
|
||||
68
web/src/components/DialogTorrentDetailsContent/widgets.jsx
Normal file
68
web/src/components/DialogTorrentDetailsContent/widgets.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
ArrowDownward as ArrowDownwardIcon,
|
||||
ArrowUpward as ArrowUpwardIcon,
|
||||
SwapVerticalCircle as SwapVerticalCircleIcon,
|
||||
ViewAgenda as ViewAgendaIcon,
|
||||
Widgets as WidgetsIcon,
|
||||
PhotoSizeSelectSmall as PhotoSizeSelectSmallIcon,
|
||||
Build as BuildIcon,
|
||||
} from '@material-ui/icons'
|
||||
import { getPeerString, humanizeSize } from 'utils/Utils'
|
||||
|
||||
import StatisticsField from './StatisticsField'
|
||||
|
||||
export const DownlodSpeedWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Download speed'
|
||||
value={humanizeSize(data) || '0 B'}
|
||||
iconBg='#118f00'
|
||||
valueBg='#13a300'
|
||||
icon={ArrowDownwardIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
export const UploadSpeedWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Upload speed'
|
||||
value={humanizeSize(data) || '0 B'}
|
||||
iconBg='#0146ad'
|
||||
valueBg='#0058db'
|
||||
icon={ArrowUpwardIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
export const PeersWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Peers'
|
||||
value={getPeerString(data)}
|
||||
iconBg='#cdc118'
|
||||
valueBg='#d8cb18'
|
||||
icon={SwapVerticalCircleIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
export const PiecesCountWidget = ({ data }) => (
|
||||
<StatisticsField title='Pieces count' value={data} iconBg='#b6c95e' valueBg='#c0d076' icon={WidgetsIcon} />
|
||||
)
|
||||
export const PiecesLengthWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Pieces length'
|
||||
value={humanizeSize(data)}
|
||||
iconBg='#0982c8'
|
||||
valueBg='#098cd7'
|
||||
icon={PhotoSizeSelectSmallIcon}
|
||||
/>
|
||||
)
|
||||
export const StatusWidget = ({ data }) => (
|
||||
<StatisticsField title='Torrent status' value={data} iconBg='#aea25b' valueBg='#b4aa6e' icon={BuildIcon} />
|
||||
)
|
||||
|
||||
export const SizeWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Torrent size'
|
||||
value={humanizeSize(data)}
|
||||
iconBg='#9b01ad'
|
||||
valueBg='#ac03bf'
|
||||
icon={ViewAgendaIcon}
|
||||
/>
|
||||
)
|
||||
@@ -1,254 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { Button, ButtonGroup, Grid, List, ListItem } from '@material-ui/core'
|
||||
import CachedIcon from '@material-ui/icons/Cached'
|
||||
import LinearProgress from '@material-ui/core/LinearProgress'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import { getPeerString, humanizeSize } from 'utils/Utils'
|
||||
import { playlistTorrHost, streamHost, viewedHost } from 'utils/Hosts'
|
||||
|
||||
const style = {
|
||||
width100: {
|
||||
width: '100%',
|
||||
},
|
||||
width80: {
|
||||
width: '80%',
|
||||
},
|
||||
poster: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
}
|
||||
|
||||
export default function DialogTorrentInfo({ torrent, open }) {
|
||||
const [torrentLocalComponentValue, setTorrentLocalComponentValue] = useState(torrent)
|
||||
const [viewed, setViewed] = useState(null)
|
||||
const [progress, setProgress] = useState(-1)
|
||||
|
||||
useEffect(() => {
|
||||
setTorrentLocalComponentValue(torrent)
|
||||
if (torrentLocalComponentValue.stat === 2)
|
||||
setProgress((torrentLocalComponentValue.preloaded_bytes * 100) / torrentLocalComponentValue.preload_size)
|
||||
getViewed(torrent.hash, list => {
|
||||
if (list) {
|
||||
const lst = list.map(itm => itm.file_index)
|
||||
setViewed(lst)
|
||||
} else setViewed(null)
|
||||
})
|
||||
}, [torrent, open])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id='form-dialog-title'>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
{torrentLocalComponentValue.poster && (
|
||||
<img alt='' height='200' align='left' style={style.poster} src={torrentLocalComponentValue.poster} />
|
||||
)}
|
||||
</Grid>
|
||||
<Grid style={style.width80} item>
|
||||
{torrentLocalComponentValue.title}{' '}
|
||||
{torrentLocalComponentValue.name &&
|
||||
torrentLocalComponentValue.name !== torrentLocalComponentValue.title &&
|
||||
` | ${torrentLocalComponentValue.name}`}
|
||||
<Typography>
|
||||
<b>Peers: </b> {getPeerString(torrentLocalComponentValue)}
|
||||
<br />
|
||||
<b>Loaded: </b> {getPreload(torrentLocalComponentValue)}
|
||||
<br />
|
||||
<b>Speed: </b> {humanizeSize(torrentLocalComponentValue.download_speed)}
|
||||
<br />
|
||||
<b>Status: </b> {torrentLocalComponentValue.stat_string}
|
||||
<br />
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{torrentLocalComponentValue.stat === 2 && (
|
||||
<LinearProgress style={{ marginTop: '10px' }} variant='determinate' value={progress} />
|
||||
)}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<List>
|
||||
<ListItem key='TorrentMenu'>
|
||||
<ButtonGroup
|
||||
style={style.width100}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
aria-label='contained primary button group'
|
||||
>
|
||||
<Button
|
||||
style={style.width100}
|
||||
href={`${playlistTorrHost()}/${encodeURIComponent(
|
||||
torrentLocalComponentValue.name || torrentLocalComponentValue.title || 'file',
|
||||
)}.m3u?link=${torrentLocalComponentValue.hash}&m3u`}
|
||||
>
|
||||
Playlist
|
||||
</Button>
|
||||
<Button
|
||||
style={style.width100}
|
||||
href={`${playlistTorrHost()}/${encodeURIComponent(
|
||||
torrentLocalComponentValue.name || torrentLocalComponentValue.title || 'file',
|
||||
)}.m3u?link=${torrentLocalComponentValue.hash}&m3u&fromlast`}
|
||||
>
|
||||
Playlist after last view
|
||||
</Button>
|
||||
<Button
|
||||
style={style.width100}
|
||||
onClick={() => {
|
||||
remViews(torrentLocalComponentValue.hash)
|
||||
setViewed(null)
|
||||
}}
|
||||
>
|
||||
Remove views
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ListItem>
|
||||
|
||||
{getPlayableFile(torrentLocalComponentValue) &&
|
||||
getPlayableFile(torrentLocalComponentValue).map(file => (
|
||||
<ButtonGroup style={style.width100} disableElevation variant='contained' color='primary'>
|
||||
<Button
|
||||
style={style.width100}
|
||||
href={`${streamHost()}/${encodeURIComponent(file.path.split('\\').pop().split('/').pop())}?link=${
|
||||
torrentLocalComponentValue.hash
|
||||
}&index=${file.id}&play`}
|
||||
>
|
||||
<Typography>
|
||||
{file.path.split('\\').pop().split('/').pop()} | {humanizeSize(file.length)}{' '}
|
||||
{viewed && viewed.indexOf(file.id) !== -1 && '| ✓'}
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
fetch(`${streamHost()}?link=${torrentLocalComponentValue.hash}&index=${file.id}&preload`)
|
||||
}
|
||||
>
|
||||
<CachedIcon />
|
||||
<Typography>Preload</Typography>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function remViews(hash) {
|
||||
try {
|
||||
if (hash)
|
||||
fetch(viewedHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'rem', hash, file_index: -1 }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function getViewed(hash, callback) {
|
||||
try {
|
||||
fetch(viewedHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'list', hash }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(callback)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayableFile(torrent) {
|
||||
if (!torrent || !torrent.file_stats) return null
|
||||
return torrent.file_stats.filter(file => extPlayable.includes(getExt(file.path)))
|
||||
}
|
||||
|
||||
function getExt(filename) {
|
||||
const ext = filename.split('.').pop()
|
||||
if (ext === filename) return ''
|
||||
return ext.toLowerCase()
|
||||
}
|
||||
|
||||
function getPreload(torrent) {
|
||||
if (torrent.preloaded_bytes > 0 && torrent.preload_size > 0 && torrent.preloaded_bytes < torrent.preload_size) {
|
||||
const progress = ((torrent.preloaded_bytes * 100) / torrent.preload_size).toFixed(2)
|
||||
return `${humanizeSize(torrent.preloaded_bytes)} / ${humanizeSize(torrent.preload_size)} ${progress}%`
|
||||
}
|
||||
|
||||
if (!torrent.preloaded_bytes) return humanizeSize(0)
|
||||
|
||||
return humanizeSize(torrent.preloaded_bytes)
|
||||
}
|
||||
|
||||
const extPlayable = [
|
||||
// video
|
||||
'3g2',
|
||||
'3gp',
|
||||
'aaf',
|
||||
'asf',
|
||||
'avchd',
|
||||
'avi',
|
||||
'drc',
|
||||
'flv',
|
||||
'iso',
|
||||
'm2v',
|
||||
'm2ts',
|
||||
'm4p',
|
||||
'm4v',
|
||||
'mkv',
|
||||
'mng',
|
||||
'mov',
|
||||
'mp2',
|
||||
'mp4',
|
||||
'mpe',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'mpv',
|
||||
'mxf',
|
||||
'nsv',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'ts',
|
||||
'qt',
|
||||
'rm',
|
||||
'rmvb',
|
||||
'roq',
|
||||
'svi',
|
||||
'vob',
|
||||
'webm',
|
||||
'wmv',
|
||||
'yuv',
|
||||
// audio
|
||||
'aac',
|
||||
'aiff',
|
||||
'ape',
|
||||
'au',
|
||||
'flac',
|
||||
'gsm',
|
||||
'it',
|
||||
'm3u',
|
||||
'm4a',
|
||||
'mid',
|
||||
'mod',
|
||||
'mp3',
|
||||
'mpa',
|
||||
'pls',
|
||||
'ra',
|
||||
's3m',
|
||||
'sid',
|
||||
'wav',
|
||||
'wma',
|
||||
'xm',
|
||||
]
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Button, Dialog, DialogActions, DialogTitle } from '@material-ui/core'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import { useState } from 'react'
|
||||
import { torrentsHost } from 'utils/Hosts'
|
||||
|
||||
const fnRemoveAll = () => {
|
||||
@@ -29,13 +31,40 @@ const fnRemoveAll = () => {
|
||||
}
|
||||
|
||||
export default function RemoveAll() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const closeDialog = () => setOpen(false)
|
||||
const openDialog = () => setOpen(true)
|
||||
|
||||
return (
|
||||
<ListItem button key='Remove all' onClick={fnRemoveAll}>
|
||||
<>
|
||||
<ListItem button key='Remove all' onClick={openDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText primary='Remove all' />
|
||||
</ListItem>
|
||||
|
||||
<Dialog open={open} onClose={closeDialog}>
|
||||
<DialogTitle>Delete Torrent?</DialogTitle>
|
||||
<DialogActions>
|
||||
<Button variant='outlined' onClick={closeDialog} color='primary'>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
fnRemoveAll()
|
||||
closeDialog()
|
||||
}}
|
||||
color='primary'
|
||||
autoFocus
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export default function SettingsDialog() {
|
||||
setOpen(false)
|
||||
const sets = JSON.parse(JSON.stringify(settings))
|
||||
sets.CacheSize *= 1024 * 1024
|
||||
sets.PreloadBufferSize *= 1024 * 1024
|
||||
fetch(settingsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'set', sets }),
|
||||
@@ -51,8 +50,6 @@ export default function SettingsDialog() {
|
||||
json => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
json.CacheSize /= 1024 * 1024
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
json.PreloadBufferSize /= 1024 * 1024
|
||||
setSets(json)
|
||||
setShow(true)
|
||||
},
|
||||
@@ -135,13 +132,7 @@ export default function SettingsDialog() {
|
||||
<br />
|
||||
<br />
|
||||
<InputLabel htmlFor='RetrackersMode'>Retracker mode</InputLabel>
|
||||
<Select
|
||||
onChange={inputForm}
|
||||
type='number'
|
||||
native='true'
|
||||
id='RetrackersMode'
|
||||
value={settings.RetrackersMode}
|
||||
>
|
||||
<Select onChange={inputForm} type='number' native id='RetrackersMode' value={settings.RetrackersMode}>
|
||||
<option value={0}>Don't add retrackers</option>
|
||||
<option value={1}>Add retrackers</option>
|
||||
<option value={2}>Remove retrackers</option>
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import 'fontsource-roboto'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import HeightIcon from '@material-ui/icons/Height'
|
||||
import CloseIcon from '@material-ui/icons/Close'
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DataUsageIcon from '@material-ui/icons/DataUsage'
|
||||
import { getPeerString, humanizeSize } from 'utils/Utils'
|
||||
import { torrentsHost } from 'utils/Hosts'
|
||||
import { NoImageIcon } from 'icons'
|
||||
import DialogTorrentInfo from 'components/DialogTorrentInfo'
|
||||
import DialogCacheInfo from 'components/DialogCacheInfo'
|
||||
|
||||
import {
|
||||
StyledButton,
|
||||
TorrentCard,
|
||||
TorrentCardButtons,
|
||||
TorrentCardDescription,
|
||||
TorrentCardDescriptionContent,
|
||||
TorrentCardDescriptionLabel,
|
||||
TorrentCardPoster,
|
||||
TorrentCardDetails,
|
||||
} from './style'
|
||||
|
||||
export default function Torrent({ torrent }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showCache, setShowCache] = useState(false)
|
||||
const [torrentLocalComponentValue, setTorrentLocalComponentValue] = useState(torrent)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
setTorrentLocalComponentValue(torrent)
|
||||
}, [torrent])
|
||||
|
||||
useEffect(() => {
|
||||
if (open)
|
||||
timerID.current = setInterval(() => {
|
||||
getTorrent(torrentLocalComponentValue.hash, (torr, error) => {
|
||||
if (error) console.error(error)
|
||||
else if (torr) setTorrentLocalComponentValue(torr)
|
||||
})
|
||||
}, 1000)
|
||||
else clearInterval(timerID.current)
|
||||
|
||||
return () => {
|
||||
clearInterval(timerID.current)
|
||||
}
|
||||
}, [torrentLocalComponentValue.hash, open])
|
||||
|
||||
const { title, name, poster, torrent_size, download_speed } = torrentLocalComponentValue
|
||||
|
||||
return (
|
||||
<>
|
||||
<TorrentCard>
|
||||
<TorrentCardPoster isPoster={poster}>
|
||||
{poster ? <img src={poster} alt='poster' /> : <NoImageIcon />}
|
||||
</TorrentCardPoster>
|
||||
|
||||
<TorrentCardButtons>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(true)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<DataUsageIcon />
|
||||
<span>Cache</span>
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton onClick={() => dropTorrent(torrentLocalComponentValue)}>
|
||||
<CloseIcon />
|
||||
<span>Drop</span>
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(false)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<HeightIcon />
|
||||
<span>Details</span>
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton onClick={() => deleteTorrent(torrentLocalComponentValue)}>
|
||||
<DeleteIcon />
|
||||
<span>Delete</span>
|
||||
</StyledButton>
|
||||
</TorrentCardButtons>
|
||||
|
||||
<TorrentCardDescription>
|
||||
<span>
|
||||
<TorrentCardDescriptionLabel>Name</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent isTitle>{title || name}</TorrentCardDescriptionContent>
|
||||
</span>
|
||||
|
||||
<TorrentCardDetails>
|
||||
<span>
|
||||
<TorrentCardDescriptionLabel>Size</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>
|
||||
{torrent_size > 0 && humanizeSize(torrent_size)}
|
||||
</TorrentCardDescriptionContent>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<TorrentCardDescriptionLabel>Speed</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>
|
||||
{download_speed > 0 ? humanizeSize(download_speed) : '---'}
|
||||
</TorrentCardDescriptionContent>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<TorrentCardDescriptionLabel>Peers</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>
|
||||
{getPeerString(torrentLocalComponentValue) || '---'}
|
||||
</TorrentCardDescriptionContent>
|
||||
</span>
|
||||
</TorrentCardDetails>
|
||||
</TorrentCardDescription>
|
||||
</TorrentCard>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'>
|
||||
{!showCache ? (
|
||||
<DialogTorrentInfo torrent={(open, torrentLocalComponentValue)} />
|
||||
) : (
|
||||
<DialogCacheInfo hash={(open, torrentLocalComponentValue.hash)} />
|
||||
)}
|
||||
<DialogActions>
|
||||
<Button variant='outlined' color='primary' onClick={() => setOpen(false)}>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function getTorrent(hash, callback) {
|
||||
try {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'get', hash }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
json => {
|
||||
callback(json, null)
|
||||
},
|
||||
error => {
|
||||
callback(null, error)
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTorrent(torrent) {
|
||||
try {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
action: 'rem',
|
||||
hash: torrent.hash,
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function dropTorrent(torrent) {
|
||||
try {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
action: 'drop',
|
||||
hash: torrent.hash,
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
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%);
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
grid-template-areas:
|
||||
'poster description'
|
||||
'buttons buttons';
|
||||
grid-template-columns: 25% 1fr;
|
||||
grid-template-rows: 100px min-content;
|
||||
}
|
||||
`
|
||||
|
||||
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);
|
||||
}
|
||||
`};
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
svg {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
`
|
||||
export const TorrentCardButtons = styled.div`
|
||||
grid-area: buttons;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
`
|
||||
export const TorrentCardDescription = styled.div`
|
||||
grid-area: description;
|
||||
background: #74c39c;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
word-break: break-word;
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`
|
||||
|
||||
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;
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
font-size: 11px;
|
||||
margin-bottom: 3px;
|
||||
margin-left: 0;
|
||||
|
||||
${({ isTitle }) =>
|
||||
isTitle &&
|
||||
css`
|
||||
overflow: auto;
|
||||
height: 45px;
|
||||
`}
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
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), (max-height: 500px) {
|
||||
padding: 5px 0;
|
||||
font-size: 0.8rem;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: #2a7e54;
|
||||
}
|
||||
`
|
||||
|
||||
export const TorrentCardDetails = styled.div`
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
118
web/src/components/TorrentCard/index.jsx
Normal file
118
web/src/components/TorrentCard/index.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'fontsource-roboto'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { UnfoldMore as UnfoldMoreIcon, Close as CloseIcon, Delete as DeleteIcon } from '@material-ui/icons'
|
||||
import { getPeerString, humanizeSize, shortenText } from 'utils/Utils'
|
||||
import { torrentsHost } from 'utils/Hosts'
|
||||
import { NoImageIcon } from 'icons'
|
||||
import DialogTorrentDetailsContent from 'components/DialogTorrentDetailsContent'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import Slide from '@material-ui/core/Slide'
|
||||
import { Button, DialogActions, DialogTitle, useMediaQuery, useTheme } from '@material-ui/core'
|
||||
import axios from 'axios'
|
||||
|
||||
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style'
|
||||
|
||||
const Transition = forwardRef((props, ref) => <Slide direction='up' ref={ref} {...props} />)
|
||||
|
||||
export default function Torrent({ torrent }) {
|
||||
const [isDetailedInfoOpened, setIsDetailedInfoOpened] = useState(false)
|
||||
const [isDeleteTorrentOpened, setIsDeleteTorrentOpened] = useState(false)
|
||||
|
||||
const theme = useTheme()
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('md'))
|
||||
|
||||
const openDetailedInfo = () => setIsDetailedInfoOpened(true)
|
||||
const closeDetailedInfo = () => setIsDetailedInfoOpened(false)
|
||||
const openDeleteTorrentAlert = () => setIsDeleteTorrentOpened(true)
|
||||
const closeDeleteTorrentAlert = () => setIsDeleteTorrentOpened(false)
|
||||
|
||||
const { title, name, poster, torrent_size: torrentSize, download_speed: downloadSpeed, hash } = torrent
|
||||
|
||||
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
|
||||
const deleteTorrent = () => axios.post(torrentsHost(), { action: 'rem', hash })
|
||||
|
||||
return (
|
||||
<>
|
||||
<TorrentCard>
|
||||
<TorrentCardPoster isPoster={poster}>
|
||||
{poster ? <img src={poster} alt='poster' /> : <NoImageIcon />}
|
||||
</TorrentCardPoster>
|
||||
|
||||
<TorrentCardButtons>
|
||||
<StyledButton onClick={openDetailedInfo}>
|
||||
<UnfoldMoreIcon />
|
||||
<span>Details</span>
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton onClick={() => dropTorrent(torrent)}>
|
||||
<CloseIcon />
|
||||
<span>Drop</span>
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton onClick={openDeleteTorrentAlert}>
|
||||
<DeleteIcon />
|
||||
<span>Delete</span>
|
||||
</StyledButton>
|
||||
</TorrentCardButtons>
|
||||
|
||||
<TorrentCardDescription>
|
||||
<div className='description-title-wrapper'>
|
||||
<div className='description-section-name'>Name</div>
|
||||
<div className='description-torrent-title'>{shortenText(title || name, 100)}</div>
|
||||
</div>
|
||||
|
||||
<div className='description-statistics-wrapper'>
|
||||
<div className='description-statistics-element-wrapper'>
|
||||
<div className='description-section-name'>Size</div>
|
||||
<div className='description-statistics-element-value'>{torrentSize > 0 && humanizeSize(torrentSize)}</div>
|
||||
</div>
|
||||
|
||||
<div className='description-statistics-element-wrapper'>
|
||||
<div className='description-section-name'>Speed</div>
|
||||
<div className='description-statistics-element-value'>
|
||||
{downloadSpeed > 0 ? humanizeSize(downloadSpeed) : '---'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='description-statistics-element-wrapper'>
|
||||
<div className='description-section-name'>Peers</div>
|
||||
<div className='description-statistics-element-value'>{getPeerString(torrent) || '---'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TorrentCardDescription>
|
||||
</TorrentCard>
|
||||
|
||||
<Dialog
|
||||
open={isDetailedInfoOpened}
|
||||
onClose={closeDetailedInfo}
|
||||
fullScreen={fullScreen}
|
||||
fullWidth
|
||||
maxWidth='xl'
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<DialogTorrentDetailsContent closeDialog={closeDetailedInfo} torrent={torrent} />
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isDeleteTorrentOpened} onClose={closeDeleteTorrentAlert}>
|
||||
<DialogTitle>Delete Torrent?</DialogTitle>
|
||||
<DialogActions>
|
||||
<Button variant='outlined' onClick={closeDeleteTorrentAlert} color='primary'>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
deleteTorrent(torrent)
|
||||
closeDeleteTorrentAlert()
|
||||
}}
|
||||
color='primary'
|
||||
autoFocus
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
206
web/src/components/TorrentCard/style.js
Normal file
206
web/src/components/TorrentCard/style.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
export const TorrentCard = styled.div`
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: 120px 260px 1fr;
|
||||
grid-template-rows: 180px;
|
||||
grid-template-areas: 'poster description buttons';
|
||||
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%);
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
grid-template-areas:
|
||||
'poster description'
|
||||
'buttons buttons';
|
||||
|
||||
grid-template-columns: 70px 1fr;
|
||||
grid-template-rows: 110px max-content;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-rows: 90px max-content;
|
||||
}
|
||||
`
|
||||
|
||||
export const TorrentCardPoster = styled.div`
|
||||
grid-area: poster;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
${({ isPoster }) =>
|
||||
isPoster
|
||||
? css`
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #74c39c;
|
||||
border: 1px solid #337a57;
|
||||
|
||||
svg {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
`};
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
svg {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const TorrentCardButtons = styled.div`
|
||||
grid-area: buttons;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
`
|
||||
export const TorrentCardDescription = styled.div`
|
||||
grid-area: description;
|
||||
background: #74c39c;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
display: grid;
|
||||
grid-template-rows: 55% 1fr;
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 770px) {
|
||||
grid-template-rows: 60% 1fr;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
grid-template-rows: 56% 1fr;
|
||||
}
|
||||
|
||||
.description-title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.description-section-name {
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.4px;
|
||||
color: #216e47;
|
||||
|
||||
@media (max-width: 770px) {
|
||||
font-size: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.description-torrent-title {
|
||||
overflow: auto;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.description-statistics-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 1fr;
|
||||
align-self: end;
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
grid-template-columns: 70px 70px 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
grid-template-columns: 65px 65px 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.description-statistics-element-wrapper {
|
||||
}
|
||||
|
||||
.description-statistics-element-value {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.description-torrent-title,
|
||||
.description-statistics-element-value {
|
||||
@media (max-width: 770px) {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
font-size: 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: #268757;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||
letter-spacing: 0.009em;
|
||||
padding: 10px 20px;
|
||||
|
||||
:hover {
|
||||
background: #2a7e54;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1260px), (max-height: 500px) {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8rem;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
font-size: 0.7rem;
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
padding: 7px 10px;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,72 +1,57 @@
|
||||
import styled from 'styled-components'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { torrentsHost } from 'utils/Hosts'
|
||||
import Torrent from 'components/Torrent'
|
||||
|
||||
const TorrentListWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 350px);
|
||||
gap: 30px;
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) {
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fit, 310px);
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
grid-template-columns: minmax(min-content, 290px);
|
||||
}
|
||||
`
|
||||
|
||||
const getTorrentList = (callback, errorCallback) => {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'list' }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(callback)
|
||||
.catch(() => errorCallback())
|
||||
}
|
||||
import TorrentCard from 'components/TorrentCard'
|
||||
import axios from 'axios'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import { TorrentListWrapper, CenteredGrid } from 'App/style'
|
||||
|
||||
export default function TorrentList() {
|
||||
const [torrents, setTorrents] = useState([])
|
||||
const [offline, setOffline] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isOffline, setIsOffline] = useState(true)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
const updateTorrentList = torrs => {
|
||||
setTorrents(torrs)
|
||||
setOffline(false)
|
||||
}
|
||||
|
||||
const resetTorrentList = () => {
|
||||
setTorrents([])
|
||||
setOffline(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
timerID.current = setInterval(() => {
|
||||
getTorrentList(updateTorrentList, resetTorrentList)
|
||||
// getting torrent list
|
||||
axios
|
||||
.post(torrentsHost(), { action: 'list' })
|
||||
.then(({ data }) => {
|
||||
// updating torrent list
|
||||
setTorrents(data)
|
||||
setIsOffline(false)
|
||||
})
|
||||
.catch(() => {
|
||||
// resetting torrent list
|
||||
setTorrents([])
|
||||
setIsOffline(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearInterval(timerID.current)
|
||||
}
|
||||
return () => clearInterval(timerID.current)
|
||||
}, [])
|
||||
|
||||
if (isLoading || isOffline || !torrents.length) {
|
||||
return (
|
||||
<CenteredGrid>
|
||||
{isLoading ? (
|
||||
<CircularProgress />
|
||||
) : isOffline ? (
|
||||
<Typography>Offline</Typography>
|
||||
) : (
|
||||
!torrents.length && <Typography>No torrents added</Typography>
|
||||
)}
|
||||
</CenteredGrid>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TorrentListWrapper>
|
||||
{offline ? (
|
||||
<Typography>Offline</Typography>
|
||||
) : !torrents.length ? (
|
||||
<Typography>No torrents added</Typography>
|
||||
) : (
|
||||
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
|
||||
)}
|
||||
{torrents.map(torrent => (
|
||||
<TorrentCard key={torrent.hash} torrent={torrent} />
|
||||
))}
|
||||
</TorrentListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,40 +3,22 @@ import ListItemText from '@material-ui/core/ListItemText'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import PublishIcon from '@material-ui/icons/Publish'
|
||||
import { torrentUploadHost } from 'utils/Hosts'
|
||||
|
||||
const classes = {
|
||||
input: {
|
||||
display: 'none',
|
||||
},
|
||||
}
|
||||
import axios from 'axios'
|
||||
|
||||
export default function UploadDialog() {
|
||||
const handleCapture = ({ target }) => {
|
||||
const handleCapture = ({ target: { files } }) => {
|
||||
const [file] = files
|
||||
const data = new FormData()
|
||||
data.append('save', 'true')
|
||||
for (let i = 0; i < target.files.length; i++) {
|
||||
data.append(`file${i}`, target.files[i])
|
||||
}
|
||||
fetch(torrentUploadHost(), {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
data.append('file', file)
|
||||
axios.post(torrentUploadHost(), data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor='raised-button-file'>
|
||||
<input
|
||||
onChange={handleCapture}
|
||||
accept='*/*'
|
||||
type='file'
|
||||
className={classes.input}
|
||||
style={{ display: 'none' }}
|
||||
id='raised-button-file'
|
||||
multiple
|
||||
/>
|
||||
<input onChange={handleCapture} accept='*/*' type='file' style={{ display: 'none' }} id='raised-button-file' />
|
||||
|
||||
<ListItem button variant='raised' type='submit' component='span' className={classes.button} key='Upload file'>
|
||||
<ListItem button variant='raised' type='submit' component='span' key='Upload file'>
|
||||
<ListItemIcon>
|
||||
<PublishIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
1
web/src/torrentStates.js
Normal file
1
web/src/torrentStates.js
Normal file
@@ -0,0 +1 @@
|
||||
export const [GETTING_INFO, PRELOAD, WORKING, CLOSED, IN_DB] = [1, 2, 3, 4, 5]
|
||||
@@ -8,3 +8,6 @@ export function getPeerString(torrent) {
|
||||
if (!torrent || !torrent.connected_seeders) return ''
|
||||
return `[${torrent.connected_seeders}] ${torrent.active_peers} / ${torrent.total_peers}`
|
||||
}
|
||||
|
||||
export const shortenText = (text, sympolAmount) =>
|
||||
text.slice(0, sympolAmount) + (text.length > sympolAmount ? '...' : '')
|
||||
|
||||
@@ -1174,7 +1174,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
|
||||
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
|
||||
@@ -2748,6 +2748,13 @@ axe-core@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
|
||||
integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||
dependencies:
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@@ -4047,6 +4054,13 @@ copy-props@^2.0.1:
|
||||
each-props "^1.3.2"
|
||||
is-plain-object "^5.0.0"
|
||||
|
||||
copy-to-clipboard@^3:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
core-js-compat@^3.6.2, core-js-compat@^3.9.0, core-js-compat@^3.9.1:
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.13.0.tgz#a88f5fa81d8e9b15d7f98abc4447a4dfca2a358f"
|
||||
@@ -5727,7 +5741,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||
@@ -5927,6 +5941,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
get-node-dimensions@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823"
|
||||
integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==
|
||||
|
||||
get-own-enumerable-property-symbols@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
@@ -7826,6 +7845,11 @@ klona@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
|
||||
integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
|
||||
|
||||
konva@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/konva/-/konva-8.0.1.tgz#f34f483cdf62c36f966addc1a7484ed694313c2b"
|
||||
integrity sha512-QDppGS1L5Dhod1zjwy9GVVjeyfPBHnPncL5oRh1NyjR1mEvhrLjzflrkdW+p73uFIW9hwCDZVLGxzzjQre9izw==
|
||||
|
||||
language-subtag-registry@~0.3.2:
|
||||
version "0.3.21"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
|
||||
@@ -8041,7 +8065,7 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@^4.7.0:
|
||||
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5, lodash@^4.7.0:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -9033,6 +9057,11 @@ parse-passwd@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
||||
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
|
||||
|
||||
parse-torrent-title@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-torrent-title/-/parse-torrent-title-1.3.0.tgz#3dedea10277b17998b124a4fd67d9e190b0306b8"
|
||||
integrity sha512-R5wya73/Ef0qUhb9177Ko8nRQyN1ziWD5DPnlrDrrgcchUnmIrG//cPENunvFYRZCLDZosXTKTo7TpQ2Pgbryg==
|
||||
|
||||
parse5@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
@@ -10215,6 +10244,14 @@ react-app-polyfill@^2.0.0:
|
||||
regenerator-runtime "^0.13.7"
|
||||
whatwg-fetch "^3.4.1"
|
||||
|
||||
react-copy-to-clipboard@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4"
|
||||
integrity sha512-9S3j+m+UxDZOM0Qb8mhnT/rMR0NGSrj9A/073yz2DSxPMYhmYFBMYIdI2X4o8AjOjyFsSNxDRnCX6s/gRxpriw==
|
||||
dependencies:
|
||||
copy-to-clipboard "^3"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-dev-utils@^11.0.3:
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a"
|
||||
@@ -10245,6 +10282,11 @@ react-dev-utils@^11.0.3:
|
||||
strip-ansi "6.0.0"
|
||||
text-table "0.2.0"
|
||||
|
||||
react-div-100vh@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-div-100vh/-/react-div-100vh-0.6.0.tgz#577972d8ac17693edcd44061c1a4b5a7578e49ec"
|
||||
integrity sha512-ErV0VTNXUd8jZqofC0ExZr5u+XDD2kN2te4SbwtqsyTm0UOjVYu53kP+FalGQrTe+DoMG8VYR2dITcAFu7c/5w==
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
@@ -10269,6 +10311,33 @@ react-is@^16.7.0, react-is@^16.8.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-konva@^17.0.2-4:
|
||||
version "17.0.2-4"
|
||||
resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-4.tgz#afd0968e1295b624bf2a7a154ba294e0d5be55cd"
|
||||
integrity sha512-YvRVPT81y8sMQV1SY1/tIDetGxBK+7Rk86u4LmiyDBLLE17vD78F01b8EC3AuP3nI3hUaTblfBugUF35cm6Etg==
|
||||
dependencies:
|
||||
react-reconciler "~0.26.2"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-measure@^2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.5.2.tgz#4ffc410e8b9cb836d9455a9ff18fc1f0fca67f89"
|
||||
integrity sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
get-node-dimensions "^1.2.1"
|
||||
prop-types "^15.6.2"
|
||||
resize-observer-polyfill "^1.5.0"
|
||||
|
||||
react-reconciler@~0.26.2:
|
||||
version "0.26.2"
|
||||
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91"
|
||||
integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-refresh@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
@@ -10638,6 +10707,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
@@ -11966,6 +12040,11 @@ to-through@^2.0.0:
|
||||
dependencies:
|
||||
through2 "^2.0.3"
|
||||
|
||||
toggle-selection@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
|
||||
|
||||
toidentifier@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
|
||||
Reference in New Issue
Block a user