Merge pull request #51 from dancheskus/adaptive-design

Adaptive design
This commit is contained in:
YouROK
2021-05-24 19:33:04 +03:00
committed by GitHub
16 changed files with 23484 additions and 388 deletions

File diff suppressed because one or more lines are too long

22981
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,8 @@
"material-ui-image": "^3.3.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1"
"react-scripts": "4.0.1",
"styled-components": "^5.3.0"
},
"scripts": {
"start": "react-scripts start",
@@ -47,5 +48,10 @@
"gulp-inline-source": "^4.0.0",
"gulp-replace": "^1.0.0",
"prettier": "2.2.1"
}
},
"description": "",
"main": "gulpfile.js",
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -1,6 +1,5 @@
import React from 'react'
import CssBaseline from '@material-ui/core/CssBaseline'
import Appbar from './components/Appbar.js'
import Appbar from './components/Appbar/index.js'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
const baseTheme = createMuiTheme({
@@ -26,11 +25,9 @@ const baseTheme = createMuiTheme({
export default function App() {
return (
<React.Fragment>
<MuiThemeProvider theme={baseTheme}>
<CssBaseline />
<Appbar />
</MuiThemeProvider>
</React.Fragment>
<MuiThemeProvider theme={baseTheme}>
<CssBaseline />
<Appbar />
</MuiThemeProvider>
)
}

View File

@@ -1,93 +0,0 @@
import React from 'react'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
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 ListItemIcon from '@material-ui/core/ListItemIcon'
import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem'
import { torrentsHost } from '../utils/Hosts'
export default function AddDialog() {
const [open, setOpen] = React.useState(false)
const [magnet, setMagnet] = React.useState('')
const [title, setTitle] = React.useState('')
const [poster, setPoster] = React.useState('')
const handleClickOpen = () => {
setOpen(true)
}
const inputMagnet = (event) => {
setMagnet(event.target.value)
}
const inputTitle = (event) => {
setTitle(event.target.value)
}
const inputPoster = (event) => {
setPoster(event.target.value)
}
const handleCloseSave = () => {
try {
if (!magnet) return
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'add',
link: magnet,
title: title,
poster: poster,
save_to_db: true,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
setOpen(false)
} catch (e) {
console.log(e)
}
}
const handleClose = () => {
setOpen(false)
}
return (
<div>
<ListItem button key="Add" onClick={handleClickOpen}>
<ListItemIcon>
<LibraryAddIcon />
</ListItemIcon>
<ListItemText primary="Add" />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth={true}>
<DialogTitle id="form-dialog-title">Add Magnet</DialogTitle>
<DialogContent>
<DialogContentText>Add magnet or link to torrent file:</DialogContentText>
<TextField onChange={inputTitle} margin="dense" id="title" label="Title" type="text" fullWidth />
<TextField onChange={inputPoster} margin="dense" id="poster" label="Poster" type="url" fullWidth />
<TextField onChange={inputMagnet} autoFocus margin="dense" id="magnet" label="Magnet" type="text" fullWidth />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Cancel
</Button>
<Button onClick={handleCloseSave} color="primary" variant="outlined">
Add
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,64 @@
import { useState } from 'react'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogTitle from '@material-ui/core/DialogTitle'
import { torrentsHost } from '../../utils/Hosts'
export default function AddDialog({ handleClose }) {
const [magnet, setMagnet] = useState('')
const [title, setTitle] = useState('')
const [poster, setPoster] = useState('')
const inputMagnet = ({ target: { value } }) => setMagnet(value)
const inputTitle = ({ target: { value } }) => setTitle(value)
const inputPoster = ({ target: { value } }) => setPoster(value)
const handleCloseSave = () => {
try {
if (!magnet) return
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'add',
link: magnet,
title: title,
poster: poster,
save_to_db: true,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
handleClose()
} catch (e) {
console.log(e)
}
}
return (
<Dialog open onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
<DialogTitle id="form-dialog-title">Add magnet or link to torrent file</DialogTitle>
<DialogContent>
<TextField onChange={inputTitle} margin="dense" id="title" label="Title" type="text" fullWidth />
<TextField onChange={inputPoster} margin="dense" id="poster" label="Poster" type="url" fullWidth />
<TextField onChange={inputMagnet} autoFocus margin="dense" id="magnet" label="Magnet or torrent file link" type="text" fullWidth />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Cancel
</Button>
<Button disabled={!magnet} onClick={handleCloseSave} color="primary" variant="outlined">
Add
</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -0,0 +1,26 @@
import { useState } from 'react'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem'
import AddDialog from './AddDialog'
export default function AddDialogButton() {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleClickOpen = () => setIsDialogOpen(true)
const handleClose = () => setIsDialogOpen(false)
return (
<div>
<ListItem button key="Add" onClick={handleClickOpen}>
<ListItemIcon>
<LibraryAddIcon />
</ListItemIcon>
<ListItemText primary="Add from link" />
</ListItem>
{isDialogOpen && <AddDialog handleClose={handleClose} />}
</div>
)
}

View File

@@ -1,11 +1,10 @@
import React, { useEffect } from 'react'
import { useEffect, useState } from 'react'
import clsx from 'clsx'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { useTheme } from '@material-ui/core/styles'
import Drawer from '@material-ui/core/Drawer'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import List from '@material-ui/core/List'
import CssBaseline from '@material-ui/core/CssBaseline'
import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider'
import IconButton from '@material-ui/core/IconButton'
@@ -19,86 +18,22 @@ import ListItemText from '@material-ui/core/ListItemText'
import ListIcon from '@material-ui/icons/List'
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
import TorrentList from './TorrentList'
import { Box } from '@material-ui/core'
import TorrentList from '../TorrentList'
import AddDialog from './Add'
import RemoveAll from './RemoveAll'
import SettingsDialog from './Settings'
import AboutDialog from './About'
import { playlistAllHost, shutdownHost, torrserverHost } from '../utils/Hosts'
import DonateDialog from './Donate'
import UploadDialog from './Upload'
const drawerWidth = 240
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: 'none',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: theme.spacing(7) + 1,
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
}))
import AddDialogButton from '../Add'
import RemoveAll from '../RemoveAll'
import SettingsDialog from '../Settings'
import AboutDialog from '../About'
import { playlistAllHost, shutdownHost, torrserverHost } from '../../utils/Hosts'
import DonateDialog from '../Donate'
import UploadDialog from '../Upload'
import useStyles from './useStyles'
export default function MiniDrawer() {
const classes = useStyles()
const theme = useTheme()
const [open, setOpen] = React.useState(false)
const [tsVersion, setTSVersion] = React.useState('')
const [open, setOpen] = useState(false)
const [tsVersion, setTSVersion] = useState('')
const handleDrawerOpen = () => {
setOpen(true)
@@ -118,7 +53,6 @@ export default function MiniDrawer() {
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
@@ -142,6 +76,7 @@ export default function MiniDrawer() {
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
@@ -156,11 +91,15 @@ export default function MiniDrawer() {
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}</IconButton>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
<AddDialog />
<AddDialogButton />
<UploadDialog />
<RemoveAll />
<ListItem button component="a" key="Playlist all torrents" target="_blank" href={playlistAllHost()}>
@@ -170,10 +109,11 @@ export default function MiniDrawer() {
<ListItemText primary="Playlist all torrents" />
</ListItem>
</List>
<Divider />
<List>
<SettingsDialog />
<DonateDialog />
<AboutDialog />
<ListItem button key="Close server" onClick={() => fetch(shutdownHost())}>
<ListItemIcon>
@@ -182,12 +122,15 @@ export default function MiniDrawer() {
<ListItemText primary="Close server" />
</ListItem>
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<Box m="5em" />
<div className={classes.toolbar} />
<TorrentList />
</main>
<DonateDialog />
</div>
)
}

View File

@@ -0,0 +1,65 @@
import { makeStyles } from '@material-ui/core/styles'
const drawerWidth = 240
export default makeStyles((theme) => ({
root: {
display: 'flex',
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: 'none',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: theme.spacing(7) + 1,
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
}))

View File

@@ -6,44 +6,44 @@ import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import { cacheHost } from '../utils/Hosts'
const style = {
cache: {
paddingLeft: "6px",
paddingRight: "2px",
lineHeight: "11px",
},
piece: {
width: "12px",
height: "12px",
backgroundColor: "#eef2f4",
border: "1px solid #eef2f4",
display: "inline-block",
marginRight: "1px",
},
pieceComplete: {
backgroundColor: "#3fb57a",
borderColor: "#3fb57a",
},
pieceLoading: {
backgroundColor: "#00d0d0",
borderColor: "#00d0d0",
},
readerRange: {
borderColor: "#9a9aff !important",
},
pieceReader: {
borderColor: "#000000 !important",
},
pieceProgress: {
position: "relative",
zIndex: "1",
backgroundColor: "#009090",
// const style = {
// cache: {
// paddingLeft: "6px",
// paddingRight: "2px",
// lineHeight: "11px",
// },
// piece: {
// width: "12px",
// height: "12px",
// backgroundColor: "#eef2f4",
// border: "1px solid #eef2f4",
// display: "inline-block",
// marginRight: "1px",
// },
// pieceComplete: {
// backgroundColor: "#3fb57a",
// borderColor: "#3fb57a",
// },
// pieceLoading: {
// backgroundColor: "#00d0d0",
// borderColor: "#00d0d0",
// },
// readerRange: {
// borderColor: "#9a9aff !important",
// },
// pieceReader: {
// borderColor: "#000000 !important",
// },
// pieceProgress: {
// position: "relative",
// zIndex: "1",
// backgroundColor: "#009090",
left: "-1px",
top: "-1px",
width: "12px",
},
}
// left: "-1px",
// top: "-1px",
// width: "12px",
// },
// }
export default function DialogCacheInfo(props) {
const [hash] = React.useState(props.hash)
@@ -65,8 +65,8 @@ export default function DialogCacheInfo(props) {
}
}, [hash, props.open])
useEffect(()=>{
if (cache && cache.PiecesCount && cache.Pieces){
useEffect(() => {
if (cache && cache.PiecesCount && cache.Pieces) {
var map = [];
for (let i = 0; i < cache.PiecesCount; i++) {
var reader = 0
@@ -80,7 +80,7 @@ export default function DialogCacheInfo(props) {
prc = (cache.Pieces[i].Size / cache.Pieces[i].Length * 100).toFixed(2)
}
cache.Readers.forEach((r, k) => {
cache.Readers.forEach(r => {
if (i >= r.Start && i <= r.End && i !== r.Reader)
cls += " reader-range"
if (i === r.Reader) {
@@ -96,12 +96,12 @@ export default function DialogCacheInfo(props) {
}
setPMap(map)
}
},[cache.Pieces])
}, [cache.Pieces])
return (
<div>
<DialogTitle id="form-dialog-title">
<Typography fullWidth>
<Typography>
<b>Hash </b> {cache.Hash}
<br />
<b>Capacity </b> {humanizeSize(cache.Capacity)}
@@ -123,48 +123,55 @@ export default function DialogCacheInfo(props) {
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
</Typography>
</DialogTitle>
<DialogContent>
<div className="cache">
{pMap.map((itm) => <span className={itm.class} title={itm.info}>{itm.prc>0 && itm.prc<100 && (<div className="piece-progress" style={{height: itm.prc/100*12+"px"}}></div>)}</span>)}
{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" }}></div>
)}
</span>
))}
</div>
</DialogContent>
</div>
)
}
function getCacheMap(cache) {
if (!cache || !cache.PiecesCount) return ''
var html = ''
for (let i = 0; i < cache.PiecesCount; i++) {
html += "<span class='piece"
let info = i
var prcDiv = ""
if (cache.Pieces && cache.Pieces[i]) {
let prc = (cache.Pieces[i].Size/cache.Pieces[i].Length*100).toFixed(2)
let piece = cache.Pieces[i]
if (piece.Completed && piece.Size >= piece.Length) {
html += ' piece-complete'
info += ' 100%'
}else {
html += ' piece-loading'
info += ' ' + prc + '%'
prcDiv = "<div class='piece-progress' style='height: "+prc+"%;'></div>"
}
}
cache.Readers.forEach((r,k)=> {
if (i >= r.Start && i <= r.End && i !== r.Reader)
html += ' reader-range'
if (i === r.Reader) {
html += ' piece-reader'
info += ' reader'
}
})
html += "' title='" + info + "'>"
html += prcDiv
html += "</span>"
}
return html
}
// function getCacheMap(cache) {
// if (!cache || !cache.PiecesCount) return ''
// var html = ''
// for (let i = 0; i < cache.PiecesCount; i++) {
// html += "<span class='piece"
// let info = i
// var prcDiv = ""
// if (cache.Pieces && cache.Pieces[i]) {
// let prc = (cache.Pieces[i].Size / cache.Pieces[i].Length * 100).toFixed(2)
// let piece = cache.Pieces[i]
// if (piece.Completed && piece.Size >= piece.Length) {
// html += ' piece-complete'
// info += ' 100%'
// } else {
// html += ' piece-loading'
// info += ' ' + prc + '%'
// prcDiv = "<div class='piece-progress' style='height: " + prc + "%;'></div>"
// }
// }
// cache.Readers.forEach((r, k) => {
// if (i >= r.Start && i <= r.End && i !== r.Reader)
// html += ' reader-range'
// if (i === r.Reader) {
// html += ' piece-reader'
// info += ' reader'
// }
// })
// html += "' title='" + info + "'>"
// html += prcDiv
// html += "</span>"
// }
// return html
// }
function getCache(hash, callback) {
try {
@@ -193,19 +200,19 @@ function getCache(hash, callback) {
}
/*
{
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
"Capacity": 209715200,
"Filled": 2914808,
"PiecesLength": 4194304,
"PiecesCount": 2065,
"DownloadSpeed": 32770.860273455524,
"Pieces": {
"2064": {
"Id": 2064,
"Length": 2914808,
"Size": 162296,
"Completed": false
}
}
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
"Capacity": 209715200,
"Filled": 2914808,
"PiecesLength": 4194304,
"PiecesCount": 2065,
"DownloadSpeed": 32770.860273455524,
"Pieces": {
"2064": {
"Id": 2064,
"Length": 2914808,
"Size": 162296,
"Completed": false
}
}
}
*/

View File

@@ -20,21 +20,25 @@ export default function DonateDialog() {
const [open, setOpen] = React.useState(false)
const [snakeOpen, setSnakeOpen] = React.useState(true)
const handleClickOpen = () => {
setOpen(true)
}
// NOT USED FOR NOW
// const handleClickOpen = () => {
// setOpen(true)
// }
const handleClose = () => {
setOpen(false)
}
return (
<div>
<ListItem button key="Donate" onClick={handleClickOpen}>
{/* !!!!!!!!!!! Should be removed or moved to sidebar because it is not visible. It is hiddent behind header */}
{/* <ListItem button key="Donate" onClick={handleClickOpen}>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>
<ListItemText primary="Donate" />
</ListItem>
</ListItem> */}
{/* !!!!!!!!!!!!!!!!!!!! */}
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
<DialogTitle id="form-dialog-title">Donate</DialogTitle>
<DialogContent>
@@ -63,12 +67,12 @@ export default function DonateDialog() {
horizontal: 'center',
}}
open={snakeOpen}
onClose={()=>{setSnakeOpen(false)}}
onClose={() => { setSnakeOpen(false) }}
autoHideDuration={6000}
message="Donate?"
action={
<React.Fragment>
<IconButton size="small" aria-label="close" color="inherit" onClick={()=>{
<IconButton size="small" aria-label="close" color="inherit" onClick={() => {
setSnakeOpen(false)
setOpen(true)
}}>

View File

@@ -5,30 +5,31 @@ import ListItemText from '@material-ui/core/ListItemText'
import DeleteIcon from '@material-ui/icons/Delete'
import { torrentsHost } from '../utils/Hosts'
export default function RemoveAll() {
const fnRemoveAll = () => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'list' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((json) => {
json.forEach((torr) => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'rem', hash: torr.hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
const fnRemoveAll = () => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'list' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((json) => {
json.forEach((torr) => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'rem', hash: torr.hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
})
}
})
}
export default function RemoveAll() {
return (
<ListItem button key="Remove all" onClick={fnRemoveAll}>
<ListItemIcon>

View File

@@ -1,27 +1,27 @@
import React, { useEffect, useRef } from 'react'
import ButtonGroup from '@material-ui/core/ButtonGroup'
import { useEffect, useRef, useState } from 'react'
import Button from '@material-ui/core/Button'
import 'fontsource-roboto'
import HeightIcon from '@material-ui/icons/Height';
import CloseIcon from '@material-ui/icons/Close';
import DeleteIcon from '@material-ui/icons/Delete'
import Typography from '@material-ui/core/Typography'
import ListItem from '@material-ui/core/ListItem'
import DialogActions from '@material-ui/core/DialogActions'
import Dialog from '@material-ui/core/Dialog'
import { getPeerString, humanizeSize } from '../utils/Utils'
import { getPeerString, humanizeSize } from '../../utils/Utils'
import DialogTorrentInfo from './DialogTorrentInfo'
import { torrentsHost } from '../utils/Hosts'
import DialogCacheInfo from './DialogCacheInfo'
import DialogTorrentInfo from '../DialogTorrentInfo'
import { torrentsHost } from '../../utils/Hosts'
import DialogCacheInfo from '../DialogCacheInfo'
import DataUsageIcon from '@material-ui/icons/DataUsage'
import { NoImageIcon } from '../../icons';
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardDescriptionContent, TorrentCardDescriptionLabel, TorrentCardPoster } from './style';
export default function Torrent(props) {
const [open, setOpen] = React.useState(false)
const [showCache, setShowCache] = React.useState(false)
const [torrent, setTorrent] = React.useState(props.torrent)
const [open, setOpen] = useState(false)
const [showCache, setShowCache] = useState(false)
const [torrent, setTorrent] = useState(props.torrent)
const timerID = useRef(-1)
useEffect(() => {
@@ -43,61 +43,74 @@ export default function Torrent(props) {
}
}, [torrent.hash, open])
const { title, name, poster, torrent_size, download_speed } = torrent
return (
<div>
<ListItem>
<ButtonGroup style={{width:'100%',boxShadow:'2px 2px 2px gray'}} disableElevation variant="contained" color="primary">
<Button
style={{width: '100%', justifyContent:'start'}}
onClick={() => {
setShowCache(false)
setOpen(true)
}}
>
{torrent.poster &&
<img src={torrent.poster} alt="" align="left" style={{width: 'auto',height:'100px',margin:'0 10px 0 0',borderRadius:'5px'}}/>
}
<Typography>
{torrent.title ? torrent.title : torrent.name}
{torrent.torrent_size > 0 ? ' | ' + humanizeSize(torrent.torrent_size) : ''}
{torrent.download_speed > 0 ? ' | ' + humanizeSize(torrent.download_speed) + '/sec' : ''}
{getPeerString(torrent) ? ' | ' + getPeerString(torrent) : '' }
</Typography>
</Button>
<Button
<>
<TorrentCard>
<TorrentCardPoster isPoster={poster}>
{poster
? <img src={poster} alt="poster" />
: <NoImageIcon />}
</TorrentCardPoster>
<TorrentCardButtons>
<StyledButton
onClick={() => {
setShowCache(true)
setOpen(true)
}}
>
<DataUsageIcon />
<Typography>Cache</Typography>
</Button>
<Button
onClick={() => {
dropTorrent(torrent)
}}
Cache
</StyledButton>
<StyledButton
onClick={() => dropTorrent(torrent)}
>
<CloseIcon />
<Typography>Drop</Typography>
</Button>
<Button
onClick={() => {
deleteTorrent(torrent)
}}
Drop
</StyledButton>
<StyledButton
onClick={() => deleteTorrent(torrent)}
>
<DeleteIcon />
<Typography>Delete</Typography>
</Button>
</ButtonGroup>
</ListItem>
Delete
</StyledButton>
<StyledButton
onClick={() => {
setShowCache(false)
setOpen(true)
}}
>
<HeightIcon />
Details
</StyledButton>
</TorrentCardButtons>
<TorrentCardDescription>
<TorrentCardDescriptionLabel>Name</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>{title || name}</TorrentCardDescriptionContent>
<TorrentCardDescriptionLabel>Size</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>{torrent_size > 0 && humanizeSize(torrent_size)}</TorrentCardDescriptionContent>
<TorrentCardDescriptionLabel>Download speed</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>{download_speed > 0 ? humanizeSize(download_speed) : '---'}</TorrentCardDescriptionContent>
<TorrentCardDescriptionLabel>Peers</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>{getPeerString(torrent) || '---'}</TorrentCardDescriptionContent>
</TorrentCardDescription>
</TorrentCard>
<Dialog
open={open}
onClose={() => {
setOpen(false)
}}
onClose={() => setOpen(false)}
aria-labelledby="form-dialog-title"
fullWidth={true}
fullWidth
maxWidth={'lg'}
>
{!showCache ? <DialogTorrentInfo torrent={(open, torrent)} /> : <DialogCacheInfo hash={(open, torrent.hash)} />}
@@ -105,15 +118,13 @@ export default function Torrent(props) {
<Button
variant="outlined"
color="primary"
onClick={() => {
setOpen(false)
}}
onClick={() => setOpen(false)}
>
OK
</Button>
</DialogActions>
</Dialog>
</div>
</>
)
}

View File

@@ -0,0 +1,98 @@
import styled, { css } from 'styled-components';
export const TorrentCard = styled.div`
border: 1px solid;
border-radius: 5px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 175px minmax(min-content, 1fr);
grid-template-areas:
"poster buttons"
"description description";
gap: 10px;
padding: 10px;
background: #3fb57a;
box-shadow:
0px 2px 4px -1px rgb(0 0 0 / 20%),
0px 4px 5px 0px rgb(0 0 0 / 14%),
0px 1px 10px 0px rgb(0 0 0 / 12%);
`
export const TorrentCardPoster = styled.div`
grid-area: poster;
border-radius: 5px;
overflow: hidden;
text-align: center;
${({ isPoster }) => isPoster ? css`
img {
height: 100%;
border-radius: 5px;
}
`: css`
display: grid;
place-items: center;
background: #74c39c;
border: 1px solid;
svg {
transform: translateY(-3px);
}
`};
`
export const TorrentCardButtons = styled.div`
grid-area: buttons;
display: grid;
gap: 5px;
`
export const TorrentCardDescription = styled.div`
grid-area: description;
background: #74c39c;
border-radius: 5px;
padding: 5px;
`
export const TorrentCardDescriptionLabel = styled.div`
text-transform: uppercase;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.4px;
color: #216e47;
`
export const TorrentCardDescriptionContent = styled.div`
margin-left: 5px;
margin-bottom: 10px;
`
export const StyledButton = styled.button`
border-radius: 5px;
border: none;
cursor: pointer;
transition: 0.2s;
display: flex;
align-items: center;
text-transform: uppercase;
background: #216e47;
color: #fff;
font-size: 1rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
letter-spacing: 0.009em;
> :first-child {
margin-right: 10px;
}
@media (max-width: 600px) {
font-size: 0.7rem;
> :first-child {
margin-right: 15px;
}
}
:hover {
background: #2a7e54;
}
`

View File

@@ -1,13 +1,18 @@
import React, { useEffect, useRef } from 'react'
import Container from '@material-ui/core/Container'
import styled from 'styled-components';
import { useEffect, useRef, useState } from 'react'
import Torrent from './Torrent'
import List from '@material-ui/core/List'
import { Typography } from '@material-ui/core'
import { torrentsHost } from '../utils/Hosts'
export default function TorrentList(props, onChange) {
const [torrents, setTorrents] = React.useState([])
const [offline, setOffline] = React.useState(true)
const TorrentListWrapper = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 350px));
gap: 30px;
`
export default function TorrentList() {
const [torrents, setTorrents] = useState([])
const [offline, setOffline] = useState(true)
const timerID = useRef(-1)
useEffect(() => {
@@ -25,9 +30,11 @@ export default function TorrentList(props, onChange) {
}, [])
return (
<React.Fragment>
<Container maxWidth="lg">{!offline ? <List>{torrents && torrents.map((torrent) => <Torrent key={torrent.hash} torrent={torrent} />)}</List> : <Typography>Offline</Typography>}</Container>
</React.Fragment>
<TorrentListWrapper>
{offline ? <Typography>Offline</Typography> : (
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
)}
</TorrentListWrapper>
)
}

11
web/src/icons/index.js Normal file
View File

@@ -0,0 +1,11 @@
export const NoImageIcon = () => (
<svg height='80px' width='80px' fill="#248a57"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enableBackground="new 0 0 100 100" xmlSpace="preserve">
<g>
<path d="M18.293,93.801c0.066,0.376,0.284,0.718,0.597,0.937c0.313,0.219,0.708,0.307,1.085,0.241l70.058-12.353 c0.376-0.066,0.718-0.284,0.937-0.597c0.219-0.313,0.307-0.708,0.24-1.085l-9.502-53.891c-0.139-0.79-0.892-1.317-1.682-1.178 l-19.402,3.421L47.997,14.16c0.241-0.706,0.375-1.456,0.375-2.229c0-0.399-0.035-0.804-0.106-1.209C47.671,7.363,44.757,5,41.455,5 c-0.4,0-0.804,0.035-1.209,0.106h0c-3.359,0.595-5.723,3.509-5.723,6.812c0,0.4,0.035,0.804,0.106,1.209 c0.178,1.005,0.567,1.918,1.109,2.709l-6.875,19.061L9.968,38.228c-0.79,0.139-1.317,0.892-1.177,1.682L18.293,93.801z M40.75,7.966L40.75,7.966c0.239-0.042,0.474-0.062,0.705-0.062c1.909,0,3.612,1.373,3.953,3.324v0 c0.042,0.238,0.062,0.473,0.062,0.704c0,1.908-1.373,3.612-3.323,3.953h0.001c-0.238,0.042-0.473,0.062-0.705,0.062 c-1.908,0-3.612-1.373-3.953-3.323c-0.042-0.238-0.062-0.473-0.062-0.705C37.427,10.01,38.799,8.306,40.75,7.966z M38.059,17.96 c1.012,0.569,2.17,0.89,3.383,0.89c0.399,0,0.804-0.034,1.208-0.106h0.001c1.48-0.263,2.766-0.976,3.743-1.974l10.935,13.108 L32.16,34.315L38.059,17.96z M29.978,37.648c0.136-0.004,0.268-0.029,0.396-0.07l29.75-5.246c0.134-0.006,0.266-0.027,0.395-0.07 l18.582-3.277l8.998,51.031L20.9,91.867l-8.998-51.032L29.978,37.648z"></path>
<path d="M49.984,75.561c0.809,0,1.627-0.065,2.449-0.199l0.001,0c7.425-1.213,12.701-7.627,12.701-14.919 c0-0.809-0.065-1.627-0.199-2.449c-1.213-7.425-7.626-12.701-14.919-12.701c-0.808,0-1.627,0.065-2.45,0.199 c-7.425,1.213-12.701,7.626-12.701,14.918c0,0.808,0.065,1.627,0.199,2.449C36.278,70.284,42.692,75.561,49.984,75.561z M51.967,72.496c-0.668,0.109-1.33,0.161-1.983,0.161c-5.883,0-11.079-4.265-12.053-10.265c-0.109-0.668-0.161-1.33-0.161-1.983 c0-2.108,0.555-4.123,1.534-5.892l19.693,14.176C57.206,70.645,54.782,72.039,51.967,72.496z M48.034,48.357L48.034,48.357 c0.668-0.109,1.329-0.161,1.983-0.161c5.882,0,11.079,4.265,12.053,10.265c0.109,0.667,0.161,1.329,0.161,1.983 c0,2.109-0.556,4.127-1.536,5.897L41.001,52.163C42.791,50.21,45.217,48.814,48.034,48.357z"></path>
<polygon points="47.567,45.492 47.567,45.492 47.568,45.491 "></polygon>
</g>
</svg>
)