mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
File diff suppressed because one or more lines are too long
22981
web/package-lock.json
generated
22981
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@
|
||||
"material-ui-image": "^3.3.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.1"
|
||||
"react-scripts": "4.0.1",
|
||||
"styled-components": "^5.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -47,5 +48,10 @@
|
||||
"gulp-inline-source": "^4.0.0",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"prettier": "2.2.1"
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"main": "gulpfile.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Appbar from './components/Appbar.js'
|
||||
import Appbar from './components/Appbar/index.js'
|
||||
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
|
||||
|
||||
const baseTheme = createMuiTheme({
|
||||
@@ -26,11 +25,9 @@ const baseTheme = createMuiTheme({
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MuiThemeProvider theme={baseTheme}>
|
||||
<CssBaseline />
|
||||
<Appbar />
|
||||
</MuiThemeProvider>
|
||||
</React.Fragment>
|
||||
<MuiThemeProvider theme={baseTheme}>
|
||||
<CssBaseline />
|
||||
<Appbar />
|
||||
</MuiThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import React from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
|
||||
export default function AddDialog() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const [magnet, setMagnet] = React.useState('')
|
||||
const [title, setTitle] = React.useState('')
|
||||
const [poster, setPoster] = React.useState('')
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const inputMagnet = (event) => {
|
||||
setMagnet(event.target.value)
|
||||
}
|
||||
|
||||
const inputTitle = (event) => {
|
||||
setTitle(event.target.value)
|
||||
}
|
||||
|
||||
const inputPoster = (event) => {
|
||||
setPoster(event.target.value)
|
||||
}
|
||||
|
||||
const handleCloseSave = () => {
|
||||
try {
|
||||
if (!magnet) return
|
||||
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
action: 'add',
|
||||
link: magnet,
|
||||
title: title,
|
||||
poster: poster,
|
||||
save_to_db: true,
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
setOpen(false)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem button key="Add" onClick={handleClickOpen}>
|
||||
<ListItemIcon>
|
||||
<LibraryAddIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add" />
|
||||
</ListItem>
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth={true}>
|
||||
<DialogTitle id="form-dialog-title">Add Magnet</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Add magnet or link to torrent file:</DialogContentText>
|
||||
<TextField onChange={inputTitle} margin="dense" id="title" label="Title" type="text" fullWidth />
|
||||
<TextField onChange={inputPoster} margin="dense" id="poster" label="Poster" type="url" fullWidth />
|
||||
<TextField onChange={inputMagnet} autoFocus margin="dense" id="magnet" label="Magnet" type="text" fullWidth />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCloseSave} color="primary" variant="outlined">
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
web/src/components/Add/AddDialog.js
Normal file
64
web/src/components/Add/AddDialog.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import { torrentsHost } from '../../utils/Hosts'
|
||||
|
||||
export default function AddDialog({ handleClose }) {
|
||||
const [magnet, setMagnet] = useState('')
|
||||
const [title, setTitle] = useState('')
|
||||
const [poster, setPoster] = useState('')
|
||||
|
||||
const inputMagnet = ({ target: { value } }) => setMagnet(value)
|
||||
const inputTitle = ({ target: { value } }) => setTitle(value)
|
||||
const inputPoster = ({ target: { value } }) => setPoster(value)
|
||||
|
||||
const handleCloseSave = () => {
|
||||
try {
|
||||
if (!magnet) return
|
||||
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
action: 'add',
|
||||
link: magnet,
|
||||
title: title,
|
||||
poster: poster,
|
||||
save_to_db: true,
|
||||
}),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
handleClose()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
|
||||
<DialogTitle id="form-dialog-title">Add magnet or link to torrent file</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<TextField onChange={inputTitle} margin="dense" id="title" label="Title" type="text" fullWidth />
|
||||
<TextField onChange={inputPoster} margin="dense" id="poster" label="Poster" type="url" fullWidth />
|
||||
<TextField onChange={inputMagnet} autoFocus margin="dense" id="magnet" label="Magnet or torrent file link" type="text" fullWidth />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary" variant="outlined">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button disabled={!magnet} onClick={handleCloseSave} color="primary" variant="outlined">
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
26
web/src/components/Add/index.js
Normal file
26
web/src/components/Add/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useState } from 'react'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import AddDialog from './AddDialog'
|
||||
|
||||
export default function AddDialogButton() {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
const handleClickOpen = () => setIsDialogOpen(true)
|
||||
const handleClose = () => setIsDialogOpen(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem button key="Add" onClick={handleClickOpen}>
|
||||
<ListItemIcon>
|
||||
<LibraryAddIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add from link" />
|
||||
</ListItem>
|
||||
|
||||
{isDialogOpen && <AddDialog handleClose={handleClose} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { makeStyles, useTheme } from '@material-ui/core/styles'
|
||||
import { useTheme } from '@material-ui/core/styles'
|
||||
import Drawer from '@material-ui/core/Drawer'
|
||||
import AppBar from '@material-ui/core/AppBar'
|
||||
import Toolbar from '@material-ui/core/Toolbar'
|
||||
import List from '@material-ui/core/List'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Divider from '@material-ui/core/Divider'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
@@ -19,86 +18,22 @@ import ListItemText from '@material-ui/core/ListItemText'
|
||||
import ListIcon from '@material-ui/icons/List'
|
||||
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
|
||||
|
||||
import TorrentList from './TorrentList'
|
||||
import { Box } from '@material-ui/core'
|
||||
import TorrentList from '../TorrentList'
|
||||
|
||||
import AddDialog from './Add'
|
||||
import RemoveAll from './RemoveAll'
|
||||
import SettingsDialog from './Settings'
|
||||
import AboutDialog from './About'
|
||||
import { playlistAllHost, shutdownHost, torrserverHost } from '../utils/Hosts'
|
||||
import DonateDialog from './Donate'
|
||||
import UploadDialog from './Upload'
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 36,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: theme.spacing(7) + 1,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing(9) + 1,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
}))
|
||||
import AddDialogButton from '../Add'
|
||||
import RemoveAll from '../RemoveAll'
|
||||
import SettingsDialog from '../Settings'
|
||||
import AboutDialog from '../About'
|
||||
import { playlistAllHost, shutdownHost, torrserverHost } from '../../utils/Hosts'
|
||||
import DonateDialog from '../Donate'
|
||||
import UploadDialog from '../Upload'
|
||||
import useStyles from './useStyles'
|
||||
|
||||
export default function MiniDrawer() {
|
||||
const classes = useStyles()
|
||||
const theme = useTheme()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [tsVersion, setTSVersion] = React.useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [tsVersion, setTSVersion] = useState('')
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true)
|
||||
@@ -118,7 +53,6 @@ export default function MiniDrawer() {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="fixed"
|
||||
className={clsx(classes.appBar, {
|
||||
@@ -142,6 +76,7 @@ export default function MiniDrawer() {
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
className={clsx(classes.drawer, {
|
||||
@@ -156,11 +91,15 @@ export default function MiniDrawer() {
|
||||
}}
|
||||
>
|
||||
<div className={classes.toolbar}>
|
||||
<IconButton onClick={handleDrawerClose}>{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}</IconButton>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<AddDialog />
|
||||
<AddDialogButton />
|
||||
<UploadDialog />
|
||||
<RemoveAll />
|
||||
<ListItem button component="a" key="Playlist all torrents" target="_blank" href={playlistAllHost()}>
|
||||
@@ -170,10 +109,11 @@ export default function MiniDrawer() {
|
||||
<ListItemText primary="Playlist all torrents" />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
<SettingsDialog />
|
||||
<DonateDialog />
|
||||
<AboutDialog />
|
||||
<ListItem button key="Close server" onClick={() => fetch(shutdownHost())}>
|
||||
<ListItemIcon>
|
||||
@@ -182,12 +122,15 @@ export default function MiniDrawer() {
|
||||
<ListItemText primary="Close server" />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
</Drawer>
|
||||
|
||||
<main className={classes.content}>
|
||||
<Box m="5em" />
|
||||
<div className={classes.toolbar} />
|
||||
|
||||
<TorrentList />
|
||||
</main>
|
||||
|
||||
<DonateDialog />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
web/src/components/Appbar/useStyles.js
Normal file
65
web/src/components/Appbar/useStyles.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
export default makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: 36,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
drawerOpen: {
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerClose: {
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
width: theme.spacing(7) + 1,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing(9) + 1,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
}))
|
||||
@@ -6,44 +6,44 @@ import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import { cacheHost } from '../utils/Hosts'
|
||||
|
||||
const style = {
|
||||
cache: {
|
||||
paddingLeft: "6px",
|
||||
paddingRight: "2px",
|
||||
lineHeight: "11px",
|
||||
},
|
||||
piece: {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
backgroundColor: "#eef2f4",
|
||||
border: "1px solid #eef2f4",
|
||||
display: "inline-block",
|
||||
marginRight: "1px",
|
||||
},
|
||||
pieceComplete: {
|
||||
backgroundColor: "#3fb57a",
|
||||
borderColor: "#3fb57a",
|
||||
},
|
||||
pieceLoading: {
|
||||
backgroundColor: "#00d0d0",
|
||||
borderColor: "#00d0d0",
|
||||
},
|
||||
readerRange: {
|
||||
borderColor: "#9a9aff !important",
|
||||
},
|
||||
pieceReader: {
|
||||
borderColor: "#000000 !important",
|
||||
},
|
||||
pieceProgress: {
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
backgroundColor: "#009090",
|
||||
// const style = {
|
||||
// cache: {
|
||||
// paddingLeft: "6px",
|
||||
// paddingRight: "2px",
|
||||
// lineHeight: "11px",
|
||||
// },
|
||||
// piece: {
|
||||
// width: "12px",
|
||||
// height: "12px",
|
||||
// backgroundColor: "#eef2f4",
|
||||
// border: "1px solid #eef2f4",
|
||||
// display: "inline-block",
|
||||
// marginRight: "1px",
|
||||
// },
|
||||
// pieceComplete: {
|
||||
// backgroundColor: "#3fb57a",
|
||||
// borderColor: "#3fb57a",
|
||||
// },
|
||||
// pieceLoading: {
|
||||
// backgroundColor: "#00d0d0",
|
||||
// borderColor: "#00d0d0",
|
||||
// },
|
||||
// readerRange: {
|
||||
// borderColor: "#9a9aff !important",
|
||||
// },
|
||||
// pieceReader: {
|
||||
// borderColor: "#000000 !important",
|
||||
// },
|
||||
// pieceProgress: {
|
||||
// position: "relative",
|
||||
// zIndex: "1",
|
||||
// backgroundColor: "#009090",
|
||||
|
||||
left: "-1px",
|
||||
top: "-1px",
|
||||
width: "12px",
|
||||
},
|
||||
}
|
||||
// left: "-1px",
|
||||
// top: "-1px",
|
||||
// width: "12px",
|
||||
// },
|
||||
// }
|
||||
|
||||
export default function DialogCacheInfo(props) {
|
||||
const [hash] = React.useState(props.hash)
|
||||
@@ -65,8 +65,8 @@ export default function DialogCacheInfo(props) {
|
||||
}
|
||||
}, [hash, props.open])
|
||||
|
||||
useEffect(()=>{
|
||||
if (cache && cache.PiecesCount && cache.Pieces){
|
||||
useEffect(() => {
|
||||
if (cache && cache.PiecesCount && cache.Pieces) {
|
||||
var map = [];
|
||||
for (let i = 0; i < cache.PiecesCount; i++) {
|
||||
var reader = 0
|
||||
@@ -80,7 +80,7 @@ export default function DialogCacheInfo(props) {
|
||||
prc = (cache.Pieces[i].Size / cache.Pieces[i].Length * 100).toFixed(2)
|
||||
}
|
||||
|
||||
cache.Readers.forEach((r, k) => {
|
||||
cache.Readers.forEach(r => {
|
||||
if (i >= r.Start && i <= r.End && i !== r.Reader)
|
||||
cls += " reader-range"
|
||||
if (i === r.Reader) {
|
||||
@@ -96,12 +96,12 @@ export default function DialogCacheInfo(props) {
|
||||
}
|
||||
setPMap(map)
|
||||
}
|
||||
},[cache.Pieces])
|
||||
}, [cache.Pieces])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
<Typography fullWidth>
|
||||
<Typography>
|
||||
<b>Hash </b> {cache.Hash}
|
||||
<br />
|
||||
<b>Capacity </b> {humanizeSize(cache.Capacity)}
|
||||
@@ -123,48 +123,55 @@ export default function DialogCacheInfo(props) {
|
||||
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<div className="cache">
|
||||
{pMap.map((itm) => <span className={itm.class} title={itm.info}>{itm.prc>0 && itm.prc<100 && (<div className="piece-progress" style={{height: itm.prc/100*12+"px"}}></div>)}</span>)}
|
||||
{pMap.map(itm => (
|
||||
<span key={itm.info} className={itm.class} title={itm.info}>
|
||||
{itm.prc > 0 && itm.prc < 100 && (
|
||||
<div className="piece-progress" style={{ height: itm.prc / 100 * 12 + "px" }}></div>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getCacheMap(cache) {
|
||||
if (!cache || !cache.PiecesCount) return ''
|
||||
var html = ''
|
||||
for (let i = 0; i < cache.PiecesCount; i++) {
|
||||
html += "<span class='piece"
|
||||
let info = i
|
||||
var prcDiv = ""
|
||||
if (cache.Pieces && cache.Pieces[i]) {
|
||||
let prc = (cache.Pieces[i].Size/cache.Pieces[i].Length*100).toFixed(2)
|
||||
let piece = cache.Pieces[i]
|
||||
if (piece.Completed && piece.Size >= piece.Length) {
|
||||
html += ' piece-complete'
|
||||
info += ' 100%'
|
||||
}else {
|
||||
html += ' piece-loading'
|
||||
info += ' ' + prc + '%'
|
||||
prcDiv = "<div class='piece-progress' style='height: "+prc+"%;'></div>"
|
||||
}
|
||||
}
|
||||
cache.Readers.forEach((r,k)=> {
|
||||
if (i >= r.Start && i <= r.End && i !== r.Reader)
|
||||
html += ' reader-range'
|
||||
if (i === r.Reader) {
|
||||
html += ' piece-reader'
|
||||
info += ' reader'
|
||||
}
|
||||
})
|
||||
html += "' title='" + info + "'>"
|
||||
html += prcDiv
|
||||
html += "</span>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
// function getCacheMap(cache) {
|
||||
// if (!cache || !cache.PiecesCount) return ''
|
||||
// var html = ''
|
||||
// for (let i = 0; i < cache.PiecesCount; i++) {
|
||||
// html += "<span class='piece"
|
||||
// let info = i
|
||||
// var prcDiv = ""
|
||||
// if (cache.Pieces && cache.Pieces[i]) {
|
||||
// let prc = (cache.Pieces[i].Size / cache.Pieces[i].Length * 100).toFixed(2)
|
||||
// let piece = cache.Pieces[i]
|
||||
// if (piece.Completed && piece.Size >= piece.Length) {
|
||||
// html += ' piece-complete'
|
||||
// info += ' 100%'
|
||||
// } else {
|
||||
// html += ' piece-loading'
|
||||
// info += ' ' + prc + '%'
|
||||
// prcDiv = "<div class='piece-progress' style='height: " + prc + "%;'></div>"
|
||||
// }
|
||||
// }
|
||||
// cache.Readers.forEach((r, k) => {
|
||||
// if (i >= r.Start && i <= r.End && i !== r.Reader)
|
||||
// html += ' reader-range'
|
||||
// if (i === r.Reader) {
|
||||
// html += ' piece-reader'
|
||||
// info += ' reader'
|
||||
// }
|
||||
// })
|
||||
// html += "' title='" + info + "'>"
|
||||
// html += prcDiv
|
||||
// html += "</span>"
|
||||
// }
|
||||
// return html
|
||||
// }
|
||||
|
||||
function getCache(hash, callback) {
|
||||
try {
|
||||
@@ -193,19 +200,19 @@ function getCache(hash, callback) {
|
||||
}
|
||||
/*
|
||||
{
|
||||
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
|
||||
"Capacity": 209715200,
|
||||
"Filled": 2914808,
|
||||
"PiecesLength": 4194304,
|
||||
"PiecesCount": 2065,
|
||||
"DownloadSpeed": 32770.860273455524,
|
||||
"Pieces": {
|
||||
"2064": {
|
||||
"Id": 2064,
|
||||
"Length": 2914808,
|
||||
"Size": 162296,
|
||||
"Completed": false
|
||||
}
|
||||
}
|
||||
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
|
||||
"Capacity": 209715200,
|
||||
"Filled": 2914808,
|
||||
"PiecesLength": 4194304,
|
||||
"PiecesCount": 2065,
|
||||
"DownloadSpeed": 32770.860273455524,
|
||||
"Pieces": {
|
||||
"2064": {
|
||||
"Id": 2064,
|
||||
"Length": 2914808,
|
||||
"Size": 162296,
|
||||
"Completed": false
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -20,21 +20,25 @@ export default function DonateDialog() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [snakeOpen, setSnakeOpen] = React.useState(true)
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
// NOT USED FOR NOW
|
||||
// const handleClickOpen = () => {
|
||||
// setOpen(true)
|
||||
// }
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem button key="Donate" onClick={handleClickOpen}>
|
||||
{/* !!!!!!!!!!! Should be removed or moved to sidebar because it is not visible. It is hiddent behind header */}
|
||||
{/* <ListItem button key="Donate" onClick={handleClickOpen}>
|
||||
<ListItemIcon>
|
||||
<CreditCardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Donate" />
|
||||
</ListItem>
|
||||
</ListItem> */}
|
||||
{/* !!!!!!!!!!!!!!!!!!!! */}
|
||||
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
|
||||
<DialogTitle id="form-dialog-title">Donate</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -63,12 +67,12 @@ export default function DonateDialog() {
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={snakeOpen}
|
||||
onClose={()=>{setSnakeOpen(false)}}
|
||||
onClose={() => { setSnakeOpen(false) }}
|
||||
autoHideDuration={6000}
|
||||
message="Donate?"
|
||||
action={
|
||||
<React.Fragment>
|
||||
<IconButton size="small" aria-label="close" color="inherit" onClick={()=>{
|
||||
<IconButton size="small" aria-label="close" color="inherit" onClick={() => {
|
||||
setSnakeOpen(false)
|
||||
setOpen(true)
|
||||
}}>
|
||||
|
||||
@@ -5,30 +5,31 @@ import ListItemText from '@material-ui/core/ListItemText'
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
|
||||
export default function RemoveAll() {
|
||||
const fnRemoveAll = () => {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'list' }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
json.forEach((torr) => {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'rem', hash: torr.hash }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const fnRemoveAll = () => {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'list' }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
json.forEach((torr) => {
|
||||
fetch(torrentsHost(), {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ action: 'rem', hash: torr.hash }),
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default function RemoveAll() {
|
||||
return (
|
||||
<ListItem button key="Remove all" onClick={fnRemoveAll}>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
|
||||
import 'fontsource-roboto'
|
||||
|
||||
import HeightIcon from '@material-ui/icons/Height';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import DeleteIcon from '@material-ui/icons/Delete'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
|
||||
import { getPeerString, humanizeSize } from '../utils/Utils'
|
||||
import { getPeerString, humanizeSize } from '../../utils/Utils'
|
||||
|
||||
import DialogTorrentInfo from './DialogTorrentInfo'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
import DialogCacheInfo from './DialogCacheInfo'
|
||||
import DialogTorrentInfo from '../DialogTorrentInfo'
|
||||
import { torrentsHost } from '../../utils/Hosts'
|
||||
import DialogCacheInfo from '../DialogCacheInfo'
|
||||
import DataUsageIcon from '@material-ui/icons/DataUsage'
|
||||
import { NoImageIcon } from '../../icons';
|
||||
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardDescriptionContent, TorrentCardDescriptionLabel, TorrentCardPoster } from './style';
|
||||
|
||||
export default function Torrent(props) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [showCache, setShowCache] = React.useState(false)
|
||||
const [torrent, setTorrent] = React.useState(props.torrent)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showCache, setShowCache] = useState(false)
|
||||
const [torrent, setTorrent] = useState(props.torrent)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,61 +43,74 @@ export default function Torrent(props) {
|
||||
}
|
||||
}, [torrent.hash, open])
|
||||
|
||||
const { title, name, poster, torrent_size, download_speed } = torrent
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListItem>
|
||||
<ButtonGroup style={{width:'100%',boxShadow:'2px 2px 2px gray'}} disableElevation variant="contained" color="primary">
|
||||
<Button
|
||||
style={{width: '100%', justifyContent:'start'}}
|
||||
onClick={() => {
|
||||
setShowCache(false)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
{torrent.poster &&
|
||||
<img src={torrent.poster} alt="" align="left" style={{width: 'auto',height:'100px',margin:'0 10px 0 0',borderRadius:'5px'}}/>
|
||||
}
|
||||
<Typography>
|
||||
{torrent.title ? torrent.title : torrent.name}
|
||||
{torrent.torrent_size > 0 ? ' | ' + humanizeSize(torrent.torrent_size) : ''}
|
||||
{torrent.download_speed > 0 ? ' | ' + humanizeSize(torrent.download_speed) + '/sec' : ''}
|
||||
{getPeerString(torrent) ? ' | ' + getPeerString(torrent) : '' }
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
<>
|
||||
|
||||
<TorrentCard>
|
||||
<TorrentCardPoster isPoster={poster}>
|
||||
{poster
|
||||
? <img src={poster} alt="poster" />
|
||||
: <NoImageIcon />}
|
||||
</TorrentCardPoster>
|
||||
|
||||
<TorrentCardButtons>
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(true)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<DataUsageIcon />
|
||||
<Typography>Cache</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dropTorrent(torrent)
|
||||
}}
|
||||
Cache
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => dropTorrent(torrent)}
|
||||
>
|
||||
<CloseIcon />
|
||||
<Typography>Drop</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteTorrent(torrent)
|
||||
}}
|
||||
Drop
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => deleteTorrent(torrent)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<Typography>Delete</Typography>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ListItem>
|
||||
Delete
|
||||
</StyledButton>
|
||||
|
||||
<StyledButton
|
||||
onClick={() => {
|
||||
setShowCache(false)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<HeightIcon />
|
||||
Details
|
||||
</StyledButton>
|
||||
</TorrentCardButtons>
|
||||
|
||||
<TorrentCardDescription>
|
||||
<TorrentCardDescriptionLabel>Name</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{title || name}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Size</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{torrent_size > 0 && humanizeSize(torrent_size)}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Download speed</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{download_speed > 0 ? humanizeSize(download_speed) : '---'}</TorrentCardDescriptionContent>
|
||||
|
||||
<TorrentCardDescriptionLabel>Peers</TorrentCardDescriptionLabel>
|
||||
<TorrentCardDescriptionContent>{getPeerString(torrent) || '---'}</TorrentCardDescriptionContent>
|
||||
</TorrentCardDescription>
|
||||
</TorrentCard>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
aria-labelledby="form-dialog-title"
|
||||
fullWidth={true}
|
||||
fullWidth
|
||||
maxWidth={'lg'}
|
||||
>
|
||||
{!showCache ? <DialogTorrentInfo torrent={(open, torrent)} /> : <DialogCacheInfo hash={(open, torrent.hash)} />}
|
||||
@@ -105,15 +118,13 @@ export default function Torrent(props) {
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
98
web/src/components/Torrent/style.js
Normal file
98
web/src/components/Torrent/style.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const TorrentCard = styled.div`
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: 175px minmax(min-content, 1fr);
|
||||
grid-template-areas:
|
||||
"poster buttons"
|
||||
"description description";
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: #3fb57a;
|
||||
box-shadow:
|
||||
0px 2px 4px -1px rgb(0 0 0 / 20%),
|
||||
0px 4px 5px 0px rgb(0 0 0 / 14%),
|
||||
0px 1px 10px 0px rgb(0 0 0 / 12%);
|
||||
`
|
||||
|
||||
export const TorrentCardPoster = styled.div`
|
||||
grid-area: poster;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
${({ isPoster }) => isPoster ? css`
|
||||
img {
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
`: css`
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #74c39c;
|
||||
border: 1px solid;
|
||||
|
||||
svg {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
`};
|
||||
`
|
||||
export const TorrentCardButtons = styled.div`
|
||||
grid-area: buttons;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
`
|
||||
export const TorrentCardDescription = styled.div`
|
||||
grid-area: description;
|
||||
background: #74c39c;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
`
|
||||
|
||||
export const TorrentCardDescriptionLabel = styled.div`
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.4px;
|
||||
color: #216e47;
|
||||
`
|
||||
|
||||
export const TorrentCardDescriptionContent = styled.div`
|
||||
margin-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
export const StyledButton = styled.button`
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
background: #216e47;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
letter-spacing: 0.009em;
|
||||
|
||||
> :first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
font-size: 0.7rem;
|
||||
|
||||
> :first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:hover {
|
||||
background: #2a7e54;
|
||||
}
|
||||
`
|
||||
@@ -1,13 +1,18 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import Container from '@material-ui/core/Container'
|
||||
import styled from 'styled-components';
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Torrent from './Torrent'
|
||||
import List from '@material-ui/core/List'
|
||||
import { Typography } from '@material-ui/core'
|
||||
import { torrentsHost } from '../utils/Hosts'
|
||||
|
||||
export default function TorrentList(props, onChange) {
|
||||
const [torrents, setTorrents] = React.useState([])
|
||||
const [offline, setOffline] = React.useState(true)
|
||||
const TorrentListWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 350px));
|
||||
gap: 30px;
|
||||
`
|
||||
|
||||
export default function TorrentList() {
|
||||
const [torrents, setTorrents] = useState([])
|
||||
const [offline, setOffline] = useState(true)
|
||||
const timerID = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,9 +30,11 @@ export default function TorrentList(props, onChange) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Container maxWidth="lg">{!offline ? <List>{torrents && torrents.map((torrent) => <Torrent key={torrent.hash} torrent={torrent} />)}</List> : <Typography>Offline</Typography>}</Container>
|
||||
</React.Fragment>
|
||||
<TorrentListWrapper>
|
||||
{offline ? <Typography>Offline</Typography> : (
|
||||
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
|
||||
)}
|
||||
</TorrentListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
11
web/src/icons/index.js
Normal file
11
web/src/icons/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const NoImageIcon = () => (
|
||||
<svg height='80px' width='80px' fill="#248a57"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enableBackground="new 0 0 100 100" xmlSpace="preserve">
|
||||
<g>
|
||||
<path d="M18.293,93.801c0.066,0.376,0.284,0.718,0.597,0.937c0.313,0.219,0.708,0.307,1.085,0.241l70.058-12.353 c0.376-0.066,0.718-0.284,0.937-0.597c0.219-0.313,0.307-0.708,0.24-1.085l-9.502-53.891c-0.139-0.79-0.892-1.317-1.682-1.178 l-19.402,3.421L47.997,14.16c0.241-0.706,0.375-1.456,0.375-2.229c0-0.399-0.035-0.804-0.106-1.209C47.671,7.363,44.757,5,41.455,5 c-0.4,0-0.804,0.035-1.209,0.106h0c-3.359,0.595-5.723,3.509-5.723,6.812c0,0.4,0.035,0.804,0.106,1.209 c0.178,1.005,0.567,1.918,1.109,2.709l-6.875,19.061L9.968,38.228c-0.79,0.139-1.317,0.892-1.177,1.682L18.293,93.801z M40.75,7.966L40.75,7.966c0.239-0.042,0.474-0.062,0.705-0.062c1.909,0,3.612,1.373,3.953,3.324v0 c0.042,0.238,0.062,0.473,0.062,0.704c0,1.908-1.373,3.612-3.323,3.953h0.001c-0.238,0.042-0.473,0.062-0.705,0.062 c-1.908,0-3.612-1.373-3.953-3.323c-0.042-0.238-0.062-0.473-0.062-0.705C37.427,10.01,38.799,8.306,40.75,7.966z M38.059,17.96 c1.012,0.569,2.17,0.89,3.383,0.89c0.399,0,0.804-0.034,1.208-0.106h0.001c1.48-0.263,2.766-0.976,3.743-1.974l10.935,13.108 L32.16,34.315L38.059,17.96z M29.978,37.648c0.136-0.004,0.268-0.029,0.396-0.07l29.75-5.246c0.134-0.006,0.266-0.027,0.395-0.07 l18.582-3.277l8.998,51.031L20.9,91.867l-8.998-51.032L29.978,37.648z"></path>
|
||||
<path d="M49.984,75.561c0.809,0,1.627-0.065,2.449-0.199l0.001,0c7.425-1.213,12.701-7.627,12.701-14.919 c0-0.809-0.065-1.627-0.199-2.449c-1.213-7.425-7.626-12.701-14.919-12.701c-0.808,0-1.627,0.065-2.45,0.199 c-7.425,1.213-12.701,7.626-12.701,14.918c0,0.808,0.065,1.627,0.199,2.449C36.278,70.284,42.692,75.561,49.984,75.561z M51.967,72.496c-0.668,0.109-1.33,0.161-1.983,0.161c-5.883,0-11.079-4.265-12.053-10.265c-0.109-0.668-0.161-1.33-0.161-1.983 c0-2.108,0.555-4.123,1.534-5.892l19.693,14.176C57.206,70.645,54.782,72.039,51.967,72.496z M48.034,48.357L48.034,48.357 c0.668-0.109,1.329-0.161,1.983-0.161c5.882,0,11.079,4.265,12.053,10.265c0.109,0.667,0.161,1.329,0.161,1.983 c0,2.109-0.556,4.127-1.536,5.897L41.001,52.163C42.791,50.21,45.217,48.814,48.034,48.357z"></path>
|
||||
<polygon points="47.567,45.492 47.567,45.492 47.568,45.491 "></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
Reference in New Issue
Block a user