mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
3
web/.env
3
web/.env
@@ -1,3 +0,0 @@
|
|||||||
INLINE_RUNTIME_CHUNK=false
|
|
||||||
GENERATE_SOURCEMAP=false
|
|
||||||
SKIP_PREFLIGHT_CHECK=true
|
|
||||||
1
web/.env_example
Normal file
1
web/.env_example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
REACT_APP_SERVER_HOST=
|
||||||
File diff suppressed because one or more lines are too long
41
web/.eslintrc
Normal file
41
web/.eslintrc
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
5
web/.gitignore
vendored
5
web/.gitignore
vendored
@@ -21,3 +21,8 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
# eslint
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": false,
|
|
||||||
"printWidth": 200,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"jsxBracketSameLine": false,
|
|
||||||
"parser": "flow",
|
|
||||||
"semi": false
|
|
||||||
}
|
|
||||||
@@ -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
6
web/jsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
4462
web/package-lock.json
generated
4462
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,14 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.2",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"fontsource-roboto": "^3.0.3",
|
"clsx": "^1.1.1",
|
||||||
"material-ui-image": "^3.3.0",
|
"fontsource-roboto": "^4.0.0",
|
||||||
"react": "^17.0.1",
|
"material-ui-image": "^3.3.2",
|
||||||
"react-dom": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-scripts": "4.0.1",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "4.0.3",
|
||||||
"styled-components": "^5.3.0"
|
"styled-components": "^5.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,12 +18,9 @@
|
|||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"build-js": "npm run build && npx gulp",
|
"build-js": "npm run build && npx gulp",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
},
|
"lint": "eslint --ext .js,.jsx src --color",
|
||||||
"eslintConfig": {
|
"fix": "eslint --ext .js,.jsx src --color --fix"
|
||||||
"extends": [
|
|
||||||
"react-app"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -37,17 +35,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.2.3",
|
"@babel/cli": "^7.14.3",
|
||||||
"@babel/core": "^7.2.2",
|
"@babel/core": "^7.14.3",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.3.4",
|
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.2.0",
|
"@babel/plugin-transform-react-jsx": "^7.14.3",
|
||||||
"babel-minify": "^0.5.0",
|
"babel-minify": "^0.5.1",
|
||||||
"babel-preset-minify": "^0.5.0",
|
"babel-preset-minify": "^0.5.1",
|
||||||
"eslint": "^7.14.0",
|
"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": "^4.0.2",
|
||||||
"gulp-inline-source": "^4.0.0",
|
"gulp-inline-source": "^4.0.0",
|
||||||
"gulp-replace": "^1.0.0",
|
"gulp-replace": "^1.1.3",
|
||||||
"prettier": "2.2.1"
|
"prettier": "2.3.0"
|
||||||
},
|
},
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "gulpfile.js",
|
"main": "gulpfile.js",
|
||||||
|
|||||||
@@ -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
34
web/src/App.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
56
web/src/components/About.jsx
Normal file
56
web/src/components/About.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
72
web/src/components/Add/AddDialog.jsx
Normal file
72
web/src/components/Add/AddDialog.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
27
web/src/components/Add/index.jsx
Normal file
27
web/src/components/Add/index.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
149
web/src/components/Appbar/index.jsx
Normal file
149
web/src/components/Appbar/index.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
|
|
||||||
const drawerWidth = 240
|
const drawerWidth = 240
|
||||||
|
|
||||||
export default makeStyles((theme) => ({
|
export default makeStyles(theme => ({
|
||||||
root: {
|
root: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
142
web/src/components/DialogCacheInfo.jsx
Normal file
142
web/src/components/DialogCacheInfo.jsx
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
254
web/src/components/DialogTorrentInfo.jsx
Normal file
254
web/src/components/DialogTorrentInfo.jsx
Normal 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',
|
||||||
|
]
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
41
web/src/components/Donate/DonateDialog.jsx
Normal file
41
web/src/components/Donate/DonateDialog.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
54
web/src/components/Donate/index.jsx
Normal file
54
web/src/components/Donate/index.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
41
web/src/components/RemoveAll.jsx
Normal file
41
web/src/components/RemoveAll.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
281
web/src/components/Settings.jsx
Normal file
281
web/src/components/Settings.jsx
Normal 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'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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
188
web/src/components/Torrent/index.jsx
Normal file
188
web/src/components/Torrent/index.jsx
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
export const TorrentCard = styled.div`
|
export const TorrentCard = styled.div`
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
@@ -7,15 +7,12 @@ export const TorrentCard = styled.div`
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
grid-template-rows: 175px minmax(min-content, 1fr);
|
grid-template-rows: 175px minmax(min-content, 1fr);
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"poster buttons"
|
'poster buttons'
|
||||||
"description description";
|
'description description';
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #3fb57a;
|
background: #3fb57a;
|
||||||
box-shadow:
|
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%);
|
||||||
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`
|
export const TorrentCardPoster = styled.div`
|
||||||
@@ -24,12 +21,15 @@ export const TorrentCardPoster = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
${({ isPoster }) => isPoster ? css`
|
${({ isPoster }) =>
|
||||||
|
isPoster
|
||||||
|
? css`
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
`: css`
|
`
|
||||||
|
: css`
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #74c39c;
|
background: #74c39c;
|
||||||
@@ -77,7 +77,7 @@ export const StyledButton = styled.button`
|
|||||||
background: #216e47;
|
background: #216e47;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||||
letter-spacing: 0.009em;
|
letter-spacing: 0.009em;
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
@@ -92,7 +92,6 @@ export const StyledButton = styled.button`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background: #2a7e54;
|
background: #2a7e54;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
55
web/src/components/TorrentList.jsx
Normal file
55
web/src/components/TorrentList.jsx
Normal 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)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
49
web/src/components/Upload.jsx
Normal file
49
web/src/components/Upload.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
22
web/src/icons/index.jsx
Normal 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>
|
||||||
|
)
|
||||||
@@ -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
12
web/src/index.jsx
Normal 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'),
|
||||||
|
)
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
export var torrserverHost = ''
|
let torrserverHost = process.env.REACT_APP_SERVER_HOST || ''
|
||||||
// export var torrserverHost = 'http://127.0.0.1:8090'
|
|
||||||
|
|
||||||
export const torrentsHost = () => torrserverHost + '/torrents'
|
export const torrentsHost = () => `${torrserverHost}/torrents`
|
||||||
export const viewedHost = () => torrserverHost + '/viewed'
|
export const viewedHost = () => `${torrserverHost}/viewed`
|
||||||
export const cacheHost = () => torrserverHost + '/cache'
|
export const cacheHost = () => `${torrserverHost}/cache`
|
||||||
export const torrentUploadHost = () => torrserverHost + '/torrent/upload'
|
export const torrentUploadHost = () => `${torrserverHost}/torrent/upload`
|
||||||
export const settingsHost = () => torrserverHost + '/settings'
|
export const settingsHost = () => `${torrserverHost}/settings`
|
||||||
export const streamHost = () => torrserverHost + '/stream'
|
export const streamHost = () => `${torrserverHost}/stream`
|
||||||
export const shutdownHost = () => torrserverHost + '/shutdown'
|
export const shutdownHost = () => `${torrserverHost}/shutdown`
|
||||||
export const echoHost = () => torrserverHost + '/echo'
|
export const echoHost = () => `${torrserverHost}/echo`
|
||||||
export const playlistAllHost = () => torrserverHost + '/playlistall/all.m3u'
|
export const playlistAllHost = () => `${torrserverHost}/playlistall/all.m3u`
|
||||||
export const playlistTorrHost = () => torrserverHost + '/stream'
|
export const playlistTorrHost = () => `${torrserverHost}/stream`
|
||||||
|
|
||||||
export const setTorrServerHost = (host) => {
|
export const getTorrServerHost = () => torrserverHost
|
||||||
|
export const setTorrServerHost = host => {
|
||||||
torrserverHost = host
|
torrserverHost = host
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export function humanizeSize(size) {
|
export function humanizeSize(size) {
|
||||||
if (!size) return ''
|
if (!size) return ''
|
||||||
var i = Math.floor(Math.log(size) / Math.log(1024))
|
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]
|
return `${(size / Math.pow(1024, i)).toFixed(2) * 1} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPeerString(torrent) {
|
export function getPeerString(torrent) {
|
||||||
if (!torrent || !torrent.connected_seeders) return ''
|
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}`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user