eslint added. lots of refactor

This commit is contained in:
Daniel Shleifman
2021-05-25 16:22:22 +03:00
parent aa0f2dc7da
commit be561767ea
46 changed files with 2369 additions and 1825 deletions

View File

@@ -1,3 +0,0 @@
INLINE_RUNTIME_CHUNK=false
GENERATE_SOURCEMAP=false
SKIP_PREFLIGHT_CHECK=true

1
web/.env_example Normal file
View File

@@ -0,0 +1 @@
REACT_APP_SERVER_HOST=

File diff suppressed because one or more lines are too long

41
web/.eslintrc Normal file
View File

@@ -0,0 +1,41 @@
{
"plugins": [ "prettier" ],
"extends": [ "airbnb", "react-app", "react-app/jest", "prettier" ],
"rules": {
"prettier/prettier": ["warn", {
"trailingComma": "all",
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120,
"arrowParens": "avoid", // Allow single argument without parentheses in arrow functions
"semi": false
}],
"import/no-anonymous-default-export": 0, // Allow "export default"
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}],
"react/jsx-one-expression-per-line": 0,
"import/order": ["warn", {
"groups": [
"external", // node_modules
"internal", // src folder
["parent", "sibling"]
],
"newlines-between": "always" // Separate all groups with new line
}],
"no-plusplus": 0,
"consistent-return": 0, // returning value is not required in arrow functions
"no-nested-ternary": 0,
"react/require-default-props": 0,
"indent": 0,
"comma-dangle": 0,
"no-shadow": 0, // Allow using same variable name in outer and function scopes
"no-unused-vars": ["warn", {
"vars": "local",
"args": "after-used",
"ignoreRestSiblings": true
}],
"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
}
}

2
web/.gitignore vendored
View File

@@ -24,3 +24,5 @@ yarn-error.log*
# eslint
.eslintcache
.env

View File

@@ -1,10 +0,0 @@
{
"useTabs": false,
"printWidth": 200,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"jsxBracketSameLine": false,
"parser": "flow",
"semi": false
}

View File

@@ -1 +1,17 @@
## TorrServer web client
# TorrServer web client
### How to start project
0. ignore first two steps if the server is on `localhost`
1. duplicate `.env_example` and rename it to `.env`
2. in `.env` file add server address to `REACT_APP_SERVER_HOST` (without last "/")
> `http://192.168.78.4:8090` - correct
>
> `http://192.168.78.4:8090/` - wrong
3. `npm start`
### Eslint
> Prettier will fix the code every time the code is saved
- `npm run lint` - to find all linting problems
- `npm run fix` - to fix code

6
web/jsconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

1066
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"dependencies": {
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"clsx": "^1.1.1",
"fontsource-roboto": "^4.0.0",
"material-ui-image": "^3.3.2",
"react": "^17.0.2",
@@ -17,12 +18,9 @@
"build": "react-scripts build",
"build-js": "npm run build && npx gulp",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app"
]
"eject": "react-scripts eject",
"lint": "eslint --ext .js,.jsx src --color",
"fix": "eslint --ext .js,.jsx src --color --fix"
},
"browserslist": {
"production": [
@@ -44,6 +42,9 @@
"babel-minify": "^0.5.1",
"babel-preset-minify": "^0.5.1",
"eslint": "^7.27.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"gulp": "^4.0.2",
"gulp-inline-source": "^4.0.0",
"gulp-replace": "^1.1.3",

View File

@@ -1,33 +0,0 @@
import CssBaseline from '@material-ui/core/CssBaseline'
import Appbar from './components/Appbar/index.js'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
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>
)
}

34
web/src/App.jsx Normal file
View File

@@ -0,0 +1,34 @@
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>
)
}

View File

@@ -1,46 +0,0 @@
import React from 'react';
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'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
export default function AboutDialog() {
const [open, setOpen] = React.useState(false)
return (
<div>
<ListItem button key="Settings" onClick={()=>{setOpen(true)}}>
<ListItemIcon>
<InfoIcon />
</ListItemIcon>
<ListItemText primary="About" />
</ListItem>
<Dialog open={open} onClose={()=>{setOpen(false)}} aria-labelledby="form-dialog-title" fullWidth={true} maxWidth={'lg'}>
<DialogTitle id="form-dialog-title">About</DialogTitle>
<DialogContent>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<center><h2>Thanks to everyone who tested and helped.</h2></center><br/>
<h2>Special thanks:</h2>
<b>Anacrolix Matt Joiner</b> <a href={'https://github.com/anacrolix/'}>github.com/anacrolix</a><br/>
<b>tsynik nikk Никита</b> <a href={'https://github.com/tsynik'}>github.com/tsynik</a><br/>
<b>Tw1cker Руслан Пахнев</b> <a href={'https://github.com/Nemiroff'}>github.com/Nemiroff</a><br/>
<b>SpAwN_LMG</b><br/>
</DialogContentText>
</DialogContent>
</DialogContent>
<DialogActions>
<Button onClick={()=>{setOpen(false)}} color="primary" variant="outlined" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,56 @@
import { useState } from 'react'
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'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
export default function AboutDialog() {
const [open, setOpen] = useState(false)
return (
<div>
<ListItem button key='Settings' onClick={() => setOpen(true)}>
<ListItemIcon>
<InfoIcon />
</ListItemIcon>
<ListItemText primary='About' />
</ListItem>
<Dialog open={open} onClose={() => setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'>
<DialogTitle id='form-dialog-title'>About</DialogTitle>
<DialogContent>
<DialogContent>
<DialogContentText id='alert-dialog-description'>
<center>
<h2>Thanks to everyone who tested and helped.</h2>
</center>
<br />
<h2>Special thanks:</h2>
<b>Anacrolix Matt Joiner</b> <a href='https://github.com/anacrolix/'>github.com/anacrolix</a>
<br />
<b>tsynik nikk Никита</b> <a href='https://github.com/tsynik'>github.com/tsynik</a>
<br />
<b>Tw1cker Руслан Пахнев</b> <a href='https://github.com/Nemiroff'>github.com/Nemiroff</a>
<br />
<b>SpAwN_LMG</b>
<br />
</DialogContentText>
</DialogContent>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color='primary' variant='outlined' autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -1,64 +0,0 @@
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,72 @@
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,
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

@@ -1,26 +0,0 @@
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

@@ -0,0 +1,27 @@
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,151 +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 TorrentList from '../TorrentList'
import AddDialogButton from '../Add'
import RemoveAll from '../RemoveAll'
import SettingsDialog from '../Settings'
import AboutDialog from '../About'
import { playlistAllHost, shutdownHost, torrserverHost } from '../../utils/Hosts'
import DonateSnackbar from '../Donate'
import DonateDialog from '../Donate/DonateDialog'
import UploadDialog from '../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(torrserverHost + '/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>
)
}

View File

@@ -0,0 +1,149 @@
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>
)
}

View File

@@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
const drawerWidth = 240
export default makeStyles((theme) => ({
export default makeStyles(theme => ({
root: {
display: 'flex',
},

View File

@@ -1,145 +0,0 @@
import React, { useEffect, useRef } from 'react'
import Typography from '@material-ui/core/Typography'
import { getPeerString, humanizeSize } from '../utils/Utils'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import { cacheHost } from '../utils/Hosts'
export default function DialogCacheInfo(props) {
const [hash] = React.useState(props.hash)
const [cache, setCache] = React.useState({})
const timerID = useRef(-1)
const [pMap, setPMap] = React.useState([])
useEffect(() => {
if (hash)
timerID.current = setInterval(() => {
getCache(hash, (cache) => {
setCache(cache)
})
}, 100)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [hash, props.open])
useEffect(() => {
if (cache && cache.PiecesCount && cache.Pieces) {
var map = [];
for (let i = 0; i < cache.PiecesCount; i++) {
var reader = 0
var cls = "piece"
var 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: prc,
class: cls,
info: i,
reader: 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" }}></div>
)}
</span>
))}
</div>
</DialogContent>
</div>
)
}
function getCache(hash, callback) {
try {
fetch(cacheHost(), {
method: 'post',
body: JSON.stringify({ action: 'get', hash: 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
}
}
}
*/

View File

@@ -0,0 +1,142 @@
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
}
}
}
*/

View File

@@ -1,229 +0,0 @@
import React, { useEffect } 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 { getPeerString, humanizeSize } from '../utils/Utils'
import { playlistTorrHost, streamHost, viewedHost } from '../utils/Hosts'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
const style = {
width100: {
width: '100%',
},
width80: {
width: '80%',
},
poster: {
display: 'flex',
flexDirection: 'row',
borderRadius: '5px',
},
}
export default function DialogTorrentInfo(props) {
const [torrent, setTorrent] = React.useState(props.torrent)
const [viewed, setViewed] = React.useState(null)
const [progress, setProgress] = React.useState(-1)
useEffect(() => {
setTorrent(props.torrent)
if(torrent.stat==2)
setProgress(torrent.preloaded_bytes * 100 / torrent.preload_size)
getViewed(props.torrent.hash,(list) => {
if (list) {
let lst = list.map((itm) => itm.file_index)
setViewed(lst)
}else
setViewed(null)
})
}, [props.torrent, props.open])
return (
<div>
<DialogTitle id="form-dialog-title">
<Grid container spacing={1}>
<Grid item>{torrent.poster && <img alt="" height="200" align="left" style={style.poster} src={torrent.poster} />}</Grid>
<Grid style={style.width80} item>
{torrent.title} {torrent.name && torrent.name !== torrent.title && ' | ' + torrent.name}
<Typography>
<b>Peers: </b> {getPeerString(torrent)}
<br />
<b>Loaded: </b> {getPreload(torrent)}
<br />
<b>Speed: </b> {humanizeSize(torrent.download_speed)}
<br />
<b>Status: </b> {torrent.stat_string}
<br />
</Typography>
</Grid>
</Grid>
{torrent.stat==2 && <LinearProgress style={{marginTop:'10px'}} variant="determinate" value={progress} />}
</DialogTitle>
<DialogContent>
<List>
<ListItem>
<ButtonGroup style={style.width100} variant="contained" color="primary" aria-label="contained primary button group">
<Button style={style.width100} href={playlistTorrHost() + '/' + encodeURIComponent(torrent.name || torrent.title || 'file') + '.m3u?link=' + torrent.hash + '&m3u'}>
Playlist
</Button>
<Button style={style.width100} href={playlistTorrHost() + '/' + encodeURIComponent(torrent.name || torrent.title || 'file') + '.m3u?link=' + torrent.hash + '&m3u&fromlast'}>
Playlist after last view
</Button>
<Button style={style.width100} onClick={()=>{
remViews(torrent.hash)
setViewed(null)
}} >
Remove views
</Button>
</ButtonGroup>
</ListItem>
{getPlayableFile(torrent) &&
getPlayableFile(torrent).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=' + torrent.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=' + torrent.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: 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: hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
callback(json)
},
(error) => {
callback(null)
}
)
} 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) {
let 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"
]

View File

@@ -0,0 +1,254 @@
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>
<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',
]

View File

@@ -1,38 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import DialogActions from '@material-ui/core/DialogActions'
import List from '@material-ui/core/List'
import ButtonGroup from '@material-ui/core/ButtonGroup'
import Button from '@material-ui/core/Button'
const donateFrame =
'<iframe src="https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=TorrServer Donate&targets-hint=&default-sum=200&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&comment=on&hint=&successURL=&quickpay=shop&account=410013733697114" width="100%" height="302" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
export default function DonateDialog ({ onClose }) {
return (
<Dialog open onClose={onClose} aria-labelledby="form-dialog-title" fullWidth>
<DialogTitle id="form-dialog-title">Donate</DialogTitle>
<DialogContent>
<List>
<ListItem>
<ButtonGroup variant="outlined" color="primary" aria-label="contained primary button group">
<Button onClick={() => window.open('https://www.paypal.com/paypalme/yourok', '_blank')}>PayPal</Button>
<Button onClick={() => window.open('https://yoomoney.ru/to/410013733697114', '_blank')}>Yandex.Money</Button>
</ButtonGroup>
</ListItem>
<ListItem>
<div dangerouslySetInnerHTML={{ __html: donateFrame }} />
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary" variant="outlined">
Ok
</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -0,0 +1,41 @@
import ListItem from '@material-ui/core/ListItem'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import DialogActions from '@material-ui/core/DialogActions'
import List from '@material-ui/core/List'
import ButtonGroup from '@material-ui/core/ButtonGroup'
import Button from '@material-ui/core/Button'
const donateFrame =
'<iframe src="https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=TorrServer Donate&targets-hint=&default-sum=200&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&comment=on&hint=&successURL=&quickpay=shop&account=410013733697114" width="100%" height="302" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
export default function DonateDialog({ onClose }) {
return (
<Dialog open onClose={onClose} aria-labelledby='form-dialog-title' fullWidth>
<DialogTitle id='form-dialog-title'>Donate</DialogTitle>
<DialogContent>
<List>
<ListItem>
<ButtonGroup variant='outlined' color='primary' aria-label='contained primary button group'>
<Button onClick={() => window.open('https://www.paypal.com/paypalme/yourok', '_blank')}>PayPal</Button>
<Button onClick={() => window.open('https://yoomoney.ru/to/410013733697114', '_blank')}>
Yandex.Money
</Button>
</ButtonGroup>
</ListItem>
<ListItem>
{/* eslint-disable-next-line react/no-danger */}
<div dangerouslySetInnerHTML={{ __html: donateFrame }} />
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color='primary' variant='outlined'>
Ok
</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -1,48 +0,0 @@
import { useState } from 'react'
import Button from '@material-ui/core/Button'
import Snackbar from '@material-ui/core/Snackbar'
import IconButton from '@material-ui/core/IconButton'
import CreditCardIcon from '@material-ui/icons/CreditCard'
import CloseIcon from '@material-ui/icons/Close'
import DonateDialog from './DonateDialog'
export default function DonateSnackbar() {
const [open, setOpen] = useState(false)
const [snackbarOpen, setSnackbarOpen] = useState(true)
const disableSnackbar = () => {
setSnackbarOpen(false)
localStorage.setItem('snackbarIsClosed', true)
}
return (
<>
{open && <DonateDialog onClose={() => setOpen(false)} />}
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
open={snackbarOpen}
onClose={disableSnackbar}
message="Donate?"
action={
<>
<Button style={{ marginRight: '10px' }} color="secondary" size="small" onClick={() => {
setOpen(true)
disableSnackbar()
}}>
<CreditCardIcon style={{ marginRight: '10px' }} fontSize="small" />
Support
</Button>
<IconButton size="small" aria-label="close" color="inherit" onClick={disableSnackbar}>
<CloseIcon fontSize="small" />
</IconButton>
</>
}
/>
</>
)
}

View File

@@ -0,0 +1,54 @@
import { useState } from 'react'
import Button from '@material-ui/core/Button'
import Snackbar from '@material-ui/core/Snackbar'
import IconButton from '@material-ui/core/IconButton'
import CreditCardIcon from '@material-ui/icons/CreditCard'
import CloseIcon from '@material-ui/icons/Close'
import DonateDialog from './DonateDialog'
export default function DonateSnackbar() {
const [open, setOpen] = useState(false)
const [snackbarOpen, setSnackbarOpen] = useState(true)
const disableSnackbar = () => {
setSnackbarOpen(false)
localStorage.setItem('snackbarIsClosed', true)
}
return (
<>
{open && <DonateDialog onClose={() => setOpen(false)} />}
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
open={snackbarOpen}
onClose={disableSnackbar}
message='Donate?'
action={
<>
<Button
style={{ marginRight: '10px' }}
color='secondary'
size='small'
onClick={() => {
setOpen(true)
disableSnackbar()
}}
>
<CreditCardIcon style={{ marginRight: '10px' }} fontSize='small' />
Support
</Button>
<IconButton size='small' aria-label='close' color='inherit' onClick={disableSnackbar}>
<CloseIcon fontSize='small' />
</IconButton>
</>
}
/>
</>
)
}

View File

@@ -1,41 +0,0 @@
import React from 'react'
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 { torrentsHost } from '../utils/Hosts'
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>
<DeleteIcon />
</ListItemIcon>
<ListItemText primary="Remove all" />
</ListItem>
)
}

View File

@@ -0,0 +1,41 @@
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 { torrentsHost } from 'utils/Hosts'
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>
<DeleteIcon />
</ListItemIcon>
<ListItemText primary='Remove all' />
</ListItem>
)
}

View File

@@ -1,162 +0,0 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import React, { useEffect } from 'react'
import SettingsIcon from '@material-ui/icons/Settings'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import TextField from '@material-ui/core/TextField'
import DialogActions from '@material-ui/core/DialogActions'
import Button from '@material-ui/core/Button'
import { FormControlLabel, InputLabel, Select, Switch } from '@material-ui/core'
import { settingsHost, setTorrServerHost, torrserverHost } from '../utils/Hosts'
export default function SettingsDialog() {
const [open, setOpen] = React.useState(false)
const [settings, setSets] = React.useState({})
const [show, setShow] = React.useState(false)
const [tsHost, setTSHost] = React.useState(torrserverHost ? torrserverHost : window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''))
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
const handleCloseSave = () => {
setOpen(false)
let sets = JSON.parse(JSON.stringify(settings))
sets.CacheSize *= 1024 * 1024
sets.PreloadBufferSize *= 1024 * 1024
fetch(settingsHost(), {
method: 'post',
body: JSON.stringify({ action: 'set', sets: sets }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
}
useEffect(() => {
fetch(settingsHost(), {
method: 'post',
body: JSON.stringify({ action: 'get' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
json.CacheSize /= 1024 * 1024
json.PreloadBufferSize /= 1024 * 1024
setSets(json)
setShow(true)
},
(error) => {
setShow(false)
console.log(error)
}
)
.catch((e) => {
setShow(false)
console.log(e)
})
}, [tsHost])
const onInputHost = (event) => {
let host = event.target.value
setTorrServerHost(host)
setTSHost(host)
}
const inputForm = (event) => {
let sets = JSON.parse(JSON.stringify(settings))
if (event.target.type === 'number' || event.target.type === 'select-one') {
sets[event.target.id] = Number(event.target.value)
} else if (event.target.type === 'checkbox') {
sets[event.target.id] = Boolean(event.target.checked)
} else if (event.target.type === 'url') {
sets[event.target.id] = event.target.value
}
setSets(sets)
}
return (
<div>
<ListItem button key="Settings" onClick={handleClickOpen}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth={true}>
<DialogTitle id="form-dialog-title">Settings</DialogTitle>
<DialogContent>
<TextField onChange={onInputHost} margin="dense" id="TorrServerHost" label="Host" value={tsHost} type="url" fullWidth />
{show && (
<>
<TextField onChange={inputForm} margin="dense" id="CacheSize" label="Cache size" value={settings.CacheSize} type="number" fullWidth />
<FormControlLabel control={<Switch checked={settings.PreloadBuffer} onChange={inputForm} id="PreloadBuffer" color="primary" />} label="Preload buffer" />
<TextField onChange={inputForm} margin="dense" id="ReaderReadAHead" label="Reader readahead" value={settings.ReaderReadAHead} type="number" fullWidth />
<h1 />
<InputLabel htmlFor="RetrackersMode">Retracker mode</InputLabel>
<Select onChange={inputForm} type="number" native="true" 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>
<option value={3}>Replace retrackers</option>
</Select>
<TextField
onChange={inputForm}
margin="dense"
id="TorrentDisconnectTimeout"
label="Torrent disconnect timeout"
value={settings.TorrentDisconnectTimeout}
type="number"
fullWidth
/>
<FormControlLabel control={<Switch checked={settings.EnableIPv6} onChange={inputForm} id="EnableIPv6" color="primary" />} label="Enable IPv6" />
<br />
<FormControlLabel control={<Switch checked={settings.ForceEncrypt} onChange={inputForm} id="ForceEncrypt" color="primary" />} label="Force encrypt" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableTCP} onChange={inputForm} id="DisableTCP" color="primary" />} label="Disable TCP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUTP} onChange={inputForm} id="DisableUTP" color="primary" />} label="Disable UTP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUPNP} onChange={inputForm} id="DisableUPNP" color="primary" />} label="Disable UPNP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableDHT} onChange={inputForm} id="DisableDHT" color="primary" />} label="Disable DHT" />
<br />
<FormControlLabel control={<Switch checked={settings.DisablePEX} onChange={inputForm} id="DisablePEX" color="primary" />} label="Disable PEX" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUpload} onChange={inputForm} id="DisableUpload" color="primary" />} label="Disable upload" />
<br />
<TextField onChange={inputForm} margin="dense" id="DownloadRateLimit" label="Download rate limit" value={settings.DownloadRateLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="UploadRateLimit" label="Upload rate limit" value={settings.UploadRateLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="ConnectionsLimit" label="Connections limit" value={settings.ConnectionsLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="DhtConnectionLimit" label="Dht connection limit" value={settings.DhtConnectionLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="PeersListenPort" label="Peers listen port" value={settings.PeersListenPort} type="number" fullWidth />
<br />
<FormControlLabel control={<Switch checked={settings.UseDisk} onChange={inputForm} id="UseDisk" color="primary" />} label="Use disk" />
<br />
<TextField onChange={inputForm} margin="dense" id="TorrentsSavePath" label="Torrents save path" value={settings.TorrentsSavePath} type="url" fullWidth />
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Cancel
</Button>
<Button onClick={handleCloseSave} color="primary" variant="outlined">
Save
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,281 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import { useEffect, useState } from 'react'
import SettingsIcon from '@material-ui/icons/Settings'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import TextField from '@material-ui/core/TextField'
import DialogActions from '@material-ui/core/DialogActions'
import Button from '@material-ui/core/Button'
import { FormControlLabel, InputLabel, Select, Switch } from '@material-ui/core'
import { settingsHost, setTorrServerHost, getTorrServerHost } from 'utils/Hosts'
export default function SettingsDialog() {
const [open, setOpen] = useState(false)
const [settings, setSets] = useState({})
const [show, setShow] = useState(false)
const { protocol, hostname, port } = window.location
const [tsHost, setTSHost] = useState(getTorrServerHost() || `${protocol}//${hostname}${port ? `:${port}` : ''}`)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
const handleCloseSave = () => {
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 }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
}
useEffect(() => {
fetch(settingsHost(), {
method: 'post',
body: JSON.stringify({ action: 'get' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(
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)
},
error => {
setShow(false)
console.log(error)
},
)
.catch(e => {
setShow(false)
console.log(e)
})
}, [tsHost])
const onInputHost = event => {
const host = event.target.value
setTorrServerHost(host)
setTSHost(host)
}
const inputForm = event => {
const sets = JSON.parse(JSON.stringify(settings))
if (event.target.type === 'number' || event.target.type === 'select-one') {
sets[event.target.id] = Number(event.target.value)
} else if (event.target.type === 'checkbox') {
sets[event.target.id] = Boolean(event.target.checked)
} else if (event.target.type === 'url') {
sets[event.target.id] = event.target.value
}
setSets(sets)
}
return (
<div>
<ListItem button key='Settings' onClick={handleClickOpen}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary='Settings' />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby='form-dialog-title' fullWidth>
<DialogTitle id='form-dialog-title'>Settings</DialogTitle>
<DialogContent>
<TextField
onChange={onInputHost}
margin='dense'
id='TorrServerHost'
label='Host'
value={tsHost}
type='url'
fullWidth
/>
{show && (
<>
<TextField
onChange={inputForm}
margin='dense'
id='CacheSize'
label='Cache size'
value={settings.CacheSize}
type='number'
fullWidth
/>
<FormControlLabel
control={
<Switch checked={settings.PreloadBuffer} onChange={inputForm} id='PreloadBuffer' color='primary' />
}
label='Preload buffer'
/>
<TextField
onChange={inputForm}
margin='dense'
id='ReaderReadAHead'
label='Reader readahead'
value={settings.ReaderReadAHead}
type='number'
fullWidth
/>
<br />
<br />
<InputLabel htmlFor='RetrackersMode'>Retracker mode</InputLabel>
<Select
onChange={inputForm}
type='number'
native='true'
id='RetrackersMode'
value={settings.RetrackersMode}
>
<option value={0}>Don&apos;t add retrackers</option>
<option value={1}>Add retrackers</option>
<option value={2}>Remove retrackers</option>
<option value={3}>Replace retrackers</option>
</Select>
<TextField
onChange={inputForm}
margin='dense'
id='TorrentDisconnectTimeout'
label='Torrent disconnect timeout'
value={settings.TorrentDisconnectTimeout}
type='number'
fullWidth
/>
<FormControlLabel
control={<Switch checked={settings.EnableIPv6} onChange={inputForm} id='EnableIPv6' color='primary' />}
label='Enable IPv6'
/>
<br />
<FormControlLabel
control={
<Switch checked={settings.ForceEncrypt} onChange={inputForm} id='ForceEncrypt' color='primary' />
}
label='Force encrypt'
/>
<br />
<FormControlLabel
control={<Switch checked={settings.DisableTCP} onChange={inputForm} id='DisableTCP' color='primary' />}
label='Disable TCP'
/>
<br />
<FormControlLabel
control={<Switch checked={settings.DisableUTP} onChange={inputForm} id='DisableUTP' color='primary' />}
label='Disable UTP'
/>
<br />
<FormControlLabel
control={
<Switch checked={settings.DisableUPNP} onChange={inputForm} id='DisableUPNP' color='primary' />
}
label='Disable UPNP'
/>
<br />
<FormControlLabel
control={<Switch checked={settings.DisableDHT} onChange={inputForm} id='DisableDHT' color='primary' />}
label='Disable DHT'
/>
<br />
<FormControlLabel
control={<Switch checked={settings.DisablePEX} onChange={inputForm} id='DisablePEX' color='primary' />}
label='Disable PEX'
/>
<br />
<FormControlLabel
control={
<Switch checked={settings.DisableUpload} onChange={inputForm} id='DisableUpload' color='primary' />
}
label='Disable upload'
/>
<br />
<TextField
onChange={inputForm}
margin='dense'
id='DownloadRateLimit'
label='Download rate limit'
value={settings.DownloadRateLimit}
type='number'
fullWidth
/>
<TextField
onChange={inputForm}
margin='dense'
id='UploadRateLimit'
label='Upload rate limit'
value={settings.UploadRateLimit}
type='number'
fullWidth
/>
<TextField
onChange={inputForm}
margin='dense'
id='ConnectionsLimit'
label='Connections limit'
value={settings.ConnectionsLimit}
type='number'
fullWidth
/>
<TextField
onChange={inputForm}
margin='dense'
id='DhtConnectionLimit'
label='Dht connection limit'
value={settings.DhtConnectionLimit}
type='number'
fullWidth
/>
<TextField
onChange={inputForm}
margin='dense'
id='PeersListenPort'
label='Peers listen port'
value={settings.PeersListenPort}
type='number'
fullWidth
/>
<br />
<FormControlLabel
control={<Switch checked={settings.UseDisk} onChange={inputForm} id='UseDisk' color='primary' />}
label='Use disk'
/>
<br />
<TextField
onChange={inputForm}
margin='dense'
id='TorrentsSavePath'
label='Torrents save path'
value={settings.TorrentsSavePath}
type='url'
fullWidth
/>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color='primary' variant='outlined'>
Cancel
</Button>
<Button onClick={handleCloseSave} color='primary' variant='outlined'>
Save
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -1,189 +0,0 @@
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 DialogActions from '@material-ui/core/DialogActions'
import Dialog from '@material-ui/core/Dialog'
import { getPeerString, humanizeSize } from '../../utils/Utils'
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] = useState(false)
const [showCache, setShowCache] = useState(false)
const [torrent, setTorrent] = useState(props.torrent)
const timerID = useRef(-1)
useEffect(() => {
setTorrent(props.torrent)
}, [props.torrent])
useEffect(() => {
if (open)
timerID.current = setInterval(() => {
getTorrent(torrent.hash, (torr, error) => {
if (error) console.error(error)
else if (torr) setTorrent(torr)
})
}, 1000)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [torrent.hash, open])
const { title, name, poster, torrent_size, download_speed } = torrent
return (
<>
<TorrentCard>
<TorrentCardPoster isPoster={poster}>
{poster
? <img src={poster} alt="poster" />
: <NoImageIcon />}
</TorrentCardPoster>
<TorrentCardButtons>
<StyledButton
onClick={() => {
setShowCache(true)
setOpen(true)
}}
>
<DataUsageIcon />
Cache
</StyledButton>
<StyledButton
onClick={() => dropTorrent(torrent)}
>
<CloseIcon />
Drop
</StyledButton>
<StyledButton
onClick={() => deleteTorrent(torrent)}
>
<DeleteIcon />
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)}
aria-labelledby="form-dialog-title"
fullWidth
maxWidth={'lg'}
>
{!showCache ? <DialogTorrentInfo torrent={(open, torrent)} /> : <DialogCacheInfo hash={(open, torrent.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: 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)
}
}

View File

@@ -0,0 +1,188 @@
/* 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,
} 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 />
Cache
</StyledButton>
<StyledButton onClick={() => dropTorrent(torrentLocalComponentValue)}>
<CloseIcon />
Drop
</StyledButton>
<StyledButton onClick={() => deleteTorrent(torrentLocalComponentValue)}>
<DeleteIcon />
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(torrentLocalComponentValue) || '---'}
</TorrentCardDescriptionContent>
</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)
}
}

View File

@@ -1,4 +1,4 @@
import styled, { css } from 'styled-components';
import styled, { css } from 'styled-components'
export const TorrentCard = styled.div`
border: 1px solid;
@@ -7,15 +7,12 @@ export const TorrentCard = styled.div`
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 175px minmax(min-content, 1fr);
grid-template-areas:
"poster buttons"
"description description";
'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%);
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`
@@ -24,12 +21,15 @@ export const TorrentCardPoster = styled.div`
overflow: hidden;
text-align: center;
${({ isPoster }) => isPoster ? css`
${({ isPoster }) =>
isPoster
? css`
img {
height: 100%;
border-radius: 5px;
}
`: css`
`
: css`
display: grid;
place-items: center;
background: #74c39c;
@@ -77,7 +77,7 @@ export const StyledButton = styled.button`
background: #216e47;
color: #fff;
font-size: 1rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
letter-spacing: 0.009em;
> :first-child {
@@ -92,7 +92,6 @@ export const StyledButton = styled.button`
}
}
:hover {
background: #2a7e54;
}

View File

@@ -1,59 +0,0 @@
import styled from 'styled-components';
import { useEffect, useRef, useState } from 'react'
import Torrent from './Torrent'
import { Typography } from '@material-ui/core'
import { torrentsHost } from '../utils/Hosts'
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(() => {
timerID.current = setInterval(() => {
getTorrentList((torrs) => {
if (torrs) setOffline(false)
else setOffline(true)
setTorrents(torrs)
})
}, 1000)
return () => {
clearInterval(timerID.current)
}
}, [])
return (
<TorrentListWrapper>
{offline ? <Typography>Offline</Typography> : (
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
)}
</TorrentListWrapper>
)
}
function getTorrentList(callback) {
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) => {
callback(json)
},
(error) => {
callback(null)
}
)
}

View File

@@ -0,0 +1,55 @@
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 './Torrent'
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(() => {
timerID.current = setInterval(() => {
getTorrentList(torrs => {
if (torrs) setOffline(false)
else setOffline(true)
setTorrents(torrs)
})
}, 1000)
return () => {
clearInterval(timerID.current)
}
}, [])
return (
<TorrentListWrapper>
{offline ? (
<Typography>Offline</Typography>
) : (
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
)}
</TorrentListWrapper>
)
}
function getTorrentList(callback) {
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)
}

View File

@@ -1,40 +0,0 @@
import React from 'react'
import ListItemIcon from '@material-ui/core/ListItemIcon'
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',
},
}
export default function UploadDialog() {
const handleCapture = ({ target }) => {
let 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,
})
}
return (
<div>
<input onChange={handleCapture} accept="*/*" type="file" className={classes.input} style={{ display: 'none' }} id="raised-button-file" multiple />
<label htmlFor="raised-button-file">
<ListItem button variant="raised" type="submit" component="span" className={classes.button} key="Upload file">
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText primary="Upload file" />
</ListItem>
</label>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import ListItemIcon from '@material-ui/core/ListItemIcon'
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',
},
}
export default function UploadDialog() {
const handleCapture = ({ target }) => {
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,
})
}
return (
<div>
<label htmlFor='raised-button-file'>
<input
onChange={handleCapture}
accept='*/*'
type='file'
className={classes.input}
style={{ display: 'none' }}
id='raised-button-file'
multiple
/>
<ListItem button variant='raised' type='submit' component='span' className={classes.button} key='Upload file'>
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText primary='Upload file' />
</ListItem>
</label>
</div>
)
}

View File

@@ -1,11 +0,0 @@
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>
)

22
web/src/icons/index.jsx Normal file
View File

@@ -0,0 +1,22 @@
// eslint-disable-next-line import/prefer-default-export
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 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' />
<polygon points='47.567,45.492 47.567,45.492 47.568,45.491 ' />
</g>
</svg>
)

View File

@@ -1,11 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

12
web/src/index.jsx Normal file
View File

@@ -0,0 +1,12 @@
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root'),
)

View File

@@ -1,17 +1,17 @@
export var torrserverHost = ''
// export var torrserverHost = 'http://127.0.0.1:8090'
let torrserverHost = process.env.REACT_APP_SERVER_HOST || ''
export const torrentsHost = () => torrserverHost + '/torrents'
export const viewedHost = () => torrserverHost + '/viewed'
export const cacheHost = () => torrserverHost + '/cache'
export const torrentUploadHost = () => torrserverHost + '/torrent/upload'
export const settingsHost = () => torrserverHost + '/settings'
export const streamHost = () => torrserverHost + '/stream'
export const shutdownHost = () => torrserverHost + '/shutdown'
export const echoHost = () => torrserverHost + '/echo'
export const playlistAllHost = () => torrserverHost + '/playlistall/all.m3u'
export const playlistTorrHost = () => torrserverHost + '/stream'
export const torrentsHost = () => `${torrserverHost}/torrents`
export const viewedHost = () => `${torrserverHost}/viewed`
export const cacheHost = () => `${torrserverHost}/cache`
export const torrentUploadHost = () => `${torrserverHost}/torrent/upload`
export const settingsHost = () => `${torrserverHost}/settings`
export const streamHost = () => `${torrserverHost}/stream`
export const shutdownHost = () => `${torrserverHost}/shutdown`
export const echoHost = () => `${torrserverHost}/echo`
export const playlistAllHost = () => `${torrserverHost}/playlistall/all.m3u`
export const playlistTorrHost = () => `${torrserverHost}/stream`
export const setTorrServerHost = (host) => {
export const getTorrServerHost = () => torrserverHost
export const setTorrServerHost = host => {
torrserverHost = host
}

View File

@@ -1,10 +1,10 @@
export function humanizeSize(size) {
if (!size) return ''
var i = Math.floor(Math.log(size) / Math.log(1024))
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
const i = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, i)).toFixed(2) * 1} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}`
}
export function getPeerString(torrent) {
if (!torrent || !torrent.connected_seeders) return ''
return '[' + torrent.connected_seeders + '] ' + torrent.active_peers + ' / ' + torrent.total_peers
return `[${torrent.connected_seeders}] ${torrent.active_peers} / ${torrent.total_peers}`
}