Merge pull request #63 from YouROK/changes-in-cache/details

Changes in cache/details. Torrent cards rewriting
This commit is contained in:
dancheskus
2021-06-02 21:24:36 +03:00
committed by GitHub
39 changed files with 2114 additions and 1158 deletions

View File

@@ -36,6 +36,7 @@
"react/prop-types": 0, "react/prop-types": 0,
"react/react-in-jsx-scope": 0, "react/react-in-jsx-scope": 0,
"react/jsx-uses-react": 0, "react/jsx-uses-react": 0,
"import/no-unresolved": 0 // used to allow relative paths from "src" folder "import/no-unresolved": 0, // used to allow relative paths from "src" folder
"react/jsx-props-no-spreading": 0
} }
} }

View File

@@ -5,11 +5,19 @@
"dependencies": { "dependencies": {
"@material-ui/core": "^4.11.4", "@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"axios": "^0.21.1",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"konva": "^8.0.1",
"lodash": "^4.17.21",
"material-ui-image": "^3.3.2", "material-ui-image": "^3.3.2",
"parse-torrent-title": "^1.3.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-div-100vh": "^0.6.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-konva": "^17.0.2-4",
"react-measure": "^2.5.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
}, },

View File

@@ -10,44 +10,6 @@
<title>TorrServer</title> <title>TorrServer</title>
</head> </head>
<body> <body>
<style>
.cache {
padding-left:6px;
padding-right:2px;
line-height:11px
}
.piece {
width:12px;
height:12px;
background-color:#eef2f4;
border:1px solid #eef2f4;
display:inline-block;
margin-right:1px
}
.piece-complete{
background-color:#3fb57a;
border-color:#3fb57a;
}
.piece-loading{
background-color:#00d0d0;
border-color:#00d0d0;
}
.reader-range{
border-color: #9a9aff !important;
}
.piece-reader{
border-color: #000000 !important;
}
.piece-progress{
position: relative;
z-index: 1;
background-color:#3fb57a;
top: -1px;
left: -1px;
width: 12px;
}
</style>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,34 +0,0 @@
import CssBaseline from '@material-ui/core/CssBaseline'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
import Appbar from './components/Appbar/index'
const baseTheme = createMuiTheme({
overrides: {
MuiCssBaseline: {
'@global': {
html: {
WebkitFontSmoothing: 'auto',
},
},
},
},
palette: {
primary: {
main: '#3fb57a',
},
secondary: {
main: '#FFA724',
},
tonalOffset: 0.2,
},
})
export default function App() {
return (
<MuiThemeProvider theme={baseTheme}>
<CssBaseline />
<Appbar />
</MuiThemeProvider>
)
}

60
web/src/App/Sidebar.jsx Normal file
View File

@@ -0,0 +1,60 @@
import { playlistAllHost, shutdownHost } from 'utils/Hosts'
import Divider from '@material-ui/core/Divider'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import AddDialogButton from 'components/Add'
import RemoveAll from 'components/RemoveAll'
import SettingsDialog from 'components/Settings'
import AboutDialog from 'components/About'
import UploadDialog from 'components/Upload'
import {
CreditCard as CreditCardIcon,
List as ListIcon,
PowerSettingsNew as PowerSettingsNewIcon,
} from '@material-ui/icons'
import List from '@material-ui/core/List'
import { AppSidebarStyle } from './style'
export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) {
return (
<AppSidebarStyle isDrawerOpen={isDrawerOpen}>
<List>
<AddDialogButton />
<UploadDialog />
<RemoveAll />
<ListItem button component='a' key='Playlist all torrents' target='_blank' href={playlistAllHost()}>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary='Playlist all torrents' />
</ListItem>
</List>
<Divider />
<List>
<SettingsDialog />
<AboutDialog />
<ListItem button key='Close server' onClick={() => fetch(shutdownHost())}>
<ListItemIcon>
<PowerSettingsNewIcon />
</ListItemIcon>
<ListItemText primary='Close server' />
</ListItem>
</List>
<Divider />
<List>
<ListItem button key='Donation' onClick={() => setIsDonationDialogOpen(true)}>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>
<ListItemText primary='Donate' />
</ListItem>
</List>
</AppSidebarStyle>
)
}

63
web/src/App/index.jsx Normal file
View File

@@ -0,0 +1,63 @@
import CssBaseline from '@material-ui/core/CssBaseline'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
import { useEffect, useState } from 'react'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import { Menu as MenuIcon, Close as CloseIcon } from '@material-ui/icons'
import { echoHost } from 'utils/Hosts'
import TorrentList from 'components/TorrentList'
import DonateSnackbar from 'components/Donate'
import DonateDialog from 'components/Donate/DonateDialog'
import Div100vh from 'react-div-100vh'
import axios from 'axios'
import { AppWrapper, AppHeader } from './style'
import Sidebar from './Sidebar'
const baseTheme = createMuiTheme({
overrides: { MuiCssBaseline: { '@global': { html: { WebkitFontSmoothing: 'auto' } } } },
palette: { primary: { main: '#3fb57a' }, secondary: { main: '#FFA724' }, tonalOffset: 0.2 },
})
export default function App() {
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [isDonationDialogOpen, setIsDonationDialogOpen] = useState(false)
const [torrServerVersion, setTorrServerVersion] = useState('')
useEffect(() => {
axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data))
}, [])
return (
<MuiThemeProvider theme={baseTheme}>
<CssBaseline />
{/* Div100vh - iOS WebKit fix */}
<Div100vh>
<AppWrapper>
<AppHeader>
<IconButton
style={{ marginRight: '20px' }}
color='inherit'
onClick={() => setIsDrawerOpen(!isDrawerOpen)}
edge='start'
>
{isDrawerOpen ? <CloseIcon /> : <MenuIcon />}
</IconButton>
<Typography variant='h6' noWrap>
TorrServer {torrServerVersion}
</Typography>
</AppHeader>
<Sidebar isDrawerOpen={isDrawerOpen} setIsDonationDialogOpen={setIsDonationDialogOpen} />
<TorrentList />
{isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />}
{!JSON.parse(localStorage.getItem('snackbarIsClosed')) && <DonateSnackbar />}
</AppWrapper>
</Div100vh>
</MuiThemeProvider>
)
}

64
web/src/App/style.js Normal file
View File

@@ -0,0 +1,64 @@
import styled, { css } from 'styled-components'
export const AppWrapper = styled.div`
height: 100%;
display: grid;
grid-template-columns: 60px 1fr;
grid-template-rows: 60px 1fr;
grid-template-areas:
'head head'
'side content';
`
export const CenteredGrid = styled.div`
height: 100%;
display: grid;
place-items: center;
`
export const AppHeader = styled.div`
background: #3fb57a;
color: rgba(0, 0, 0, 0.87);
grid-area: head;
display: flex;
align-items: center;
box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
padding: 0 24px;
z-index: 3;
`
export const AppSidebarStyle = styled.div`
${({ isDrawerOpen }) => css`
grid-area: side;
width: ${isDrawerOpen ? '400%' : '100%'};
z-index: 2;
overflow-x: hidden;
transition: width 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms;
border-right: 1px solid rgba(0, 0, 0, 0.12);
background: #fff;
white-space: nowrap;
`}
`
export const TorrentListWrapper = styled.div`
grid-area: content;
padding: 20px;
overflow: auto;
display: grid;
place-content: start;
grid-template-columns: repeat(auto-fit, minmax(max-content, 570px));
gap: 20px;
@media (max-width: 1260px), (max-height: 500px) {
padding: 10px;
gap: 15px;
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1100px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 700px) {
grid-template-columns: 1fr;
}
`

View File

@@ -3,7 +3,6 @@ import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import InfoIcon from '@material-ui/icons/Info' import InfoIcon from '@material-ui/icons/Info'
import ListItem from '@material-ui/core/ListItem' import ListItem from '@material-ui/core/ListItem'
@@ -27,23 +26,21 @@ export default function AboutDialog() {
<DialogContent> <DialogContent>
<DialogContent> <DialogContent>
<DialogContentText id='alert-dialog-description'> <center>
<center> <h2>Thanks to everyone who tested and helped.</h2>
<h2>Thanks to everyone who tested and helped.</h2> </center>
</center> <br />
<br /> <h2>Special thanks:</h2>
<h2>Special thanks:</h2> <b>Anacrolix Matt Joiner</b> <a href='https://github.com/anacrolix/'>github.com/anacrolix</a>
<b>Anacrolix Matt Joiner</b> <a href='https://github.com/anacrolix/'>github.com/anacrolix</a> <br />
<br /> <b>tsynik nikk Никита</b> <a href='https://github.com/tsynik'>github.com/tsynik</a>
<b>tsynik nikk Никита</b> <a href='https://github.com/tsynik'>github.com/tsynik</a> <br />
<br /> <b>dancheskus</b> <a href='https://github.com/dancheskus'>github.com/dancheskus</a>
<b>dancheskus</b> <a href='https://github.com/dancheskus'>github.com/dancheskus</a> <br />
<br /> <b>Tw1cker Руслан Пахнев</b> <a href='https://github.com/Nemiroff'>github.com/Nemiroff</a>
<b>Tw1cker Руслан Пахнев</b> <a href='https://github.com/Nemiroff'>github.com/Nemiroff</a> <br />
<br /> <b>SpAwN_LMG</b>
<b>SpAwN_LMG</b> <br />
<br />
</DialogContentText>
</DialogContent> </DialogContent>
</DialogContent> </DialogContent>

View File

@@ -63,7 +63,7 @@ export default function AddDialog({ handleClose }) {
Cancel Cancel
</Button> </Button>
<Button disabled={!magnet} onClick={handleCloseSave} color='primary' variant='outlined'> <Button variant='contained' disabled={!magnet} onClick={handleCloseSave} color='primary'>
Add Add
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -1,144 +0,0 @@
import { useEffect, useState } from 'react'
import clsx from 'clsx'
import { useTheme } from '@material-ui/core/styles'
import Drawer from '@material-ui/core/Drawer'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import List from '@material-ui/core/List'
import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import CreditCardIcon from '@material-ui/icons/CreditCard'
import ListIcon from '@material-ui/icons/List'
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
import { playlistAllHost, shutdownHost, getTorrServerHost } from 'utils/Hosts'
import TorrentList from 'components/TorrentList'
import AddDialogButton from 'components/Add'
import RemoveAll from 'components/RemoveAll'
import SettingsDialog from 'components/Settings'
import AboutDialog from 'components/About'
import DonateSnackbar from 'components/Donate'
import DonateDialog from 'components/Donate/DonateDialog'
import UploadDialog from 'components/Upload'
import useStyles from './useStyles'
export default function MiniDrawer() {
const classes = useStyles()
const theme = useTheme()
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [isDonationDialogOpen, setIsDonationDialogOpen] = useState(false)
const [tsVersion, setTSVersion] = useState('')
const handleDrawerOpen = () => setIsDrawerOpen(true)
const handleDrawerClose = () => setIsDrawerOpen(false)
useEffect(() => {
fetch(`${getTorrServerHost()}/echo`)
.then(resp => resp.text())
.then(txt => {
if (!txt.startsWith('<!DOCTYPE html>')) setTSVersion(txt)
})
}, [isDrawerOpen])
return (
<div className={classes.root}>
<AppBar
position='fixed'
className={clsx(classes.appBar, {
[classes.appBarShift]: isDrawerOpen,
})}
>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
onClick={handleDrawerOpen}
edge='start'
className={clsx(classes.menuButton, {
[classes.hide]: isDrawerOpen,
})}
>
<MenuIcon />
</IconButton>
<Typography variant='h6' noWrap>
TorrServer {tsVersion}
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant='permanent'
className={clsx(classes.drawer, {
[classes.drawerOpen]: isDrawerOpen,
[classes.drawerClose]: !isDrawerOpen,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: isDrawerOpen,
[classes.drawerClose]: !isDrawerOpen,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
<AddDialogButton />
<UploadDialog />
<RemoveAll />
<ListItem button component='a' key='Playlist all torrents' target='_blank' href={playlistAllHost()}>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary='Playlist all torrents' />
</ListItem>
</List>
<Divider />
<List>
<SettingsDialog />
<AboutDialog />
<ListItem button key='Close server' onClick={() => fetch(shutdownHost())}>
<ListItemIcon>
<PowerSettingsNewIcon />
</ListItemIcon>
<ListItemText primary='Close server' />
</ListItem>
</List>
<Divider />
<List>
<ListItem button key='Donation' onClick={() => setIsDonationDialogOpen(true)}>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>
<ListItemText primary='Donate' />
</ListItem>
</List>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<TorrentList />
</main>
{isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />}
{!JSON.parse(localStorage.getItem('snackbarIsClosed')) && <DonateSnackbar />}
</div>
)
}

View File

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

View File

@@ -1,142 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import Typography from '@material-ui/core/Typography'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import { getPeerString, humanizeSize } from 'utils/Utils'
import { cacheHost } from 'utils/Hosts'
export default function DialogCacheInfo({ hash, open }) {
const [cache, setCache] = useState({})
const timerID = useRef(-1)
const [pMap, setPMap] = useState([])
useEffect(() => {
if (hash)
timerID.current = setInterval(() => {
getCache(hash, cache => {
setCache(cache)
})
}, 100)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [hash, open])
useEffect(() => {
if (cache && cache.PiecesCount && cache.Pieces) {
const map = []
for (let i = 0; i < cache.PiecesCount; i++) {
const reader = 0
let cls = 'piece'
let prc = 0
if (cache.Pieces[i]) {
if (cache.Pieces[i].Completed && cache.Pieces[i].Size >= cache.Pieces[i].Length) cls += ' piece-complete'
else cls += ' piece-loading'
prc = ((cache.Pieces[i].Size / cache.Pieces[i].Length) * 100).toFixed(2)
}
cache.Readers.forEach(r => {
if (i >= r.Start && i <= r.End && i !== r.Reader) cls += ' reader-range'
if (i === r.Reader) {
cls += ' piece-reader'
}
})
map.push({
prc,
class: cls,
info: i,
reader,
})
}
setPMap(map)
}
}, [cache.Pieces])
return (
<div>
<DialogTitle id='form-dialog-title'>
<Typography>
<b>Hash </b> {cache.Hash}
<br />
<b>Capacity </b> {humanizeSize(cache.Capacity)}
<br />
<b>Filled </b> {humanizeSize(cache.Filled)}
<br />
<b>Torrent size </b> {cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)}
<br />
<b>Pieces length </b> {humanizeSize(cache.PiecesLength)}
<br />
<b>Pieces count </b> {cache.PiecesCount}
<br />
<b>Peers: </b> {getPeerString(cache.Torrent)}
<br />
<b>Download speed </b>{' '}
{cache.Torrent && cache.Torrent.download_speed ? `${humanizeSize(cache.Torrent.download_speed)}/sec` : ''}
<br />
<b>Upload speed </b>{' '}
{cache.Torrent && cache.Torrent.upload_speed ? `${humanizeSize(cache.Torrent.upload_speed)}/sec` : ''}
<br />
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
</Typography>
</DialogTitle>
<DialogContent>
<div className='cache'>
{pMap.map(itm => (
<span key={itm.info} className={itm.class} title={itm.info}>
{itm.prc > 0 && itm.prc < 100 && (
<div className='piece-progress' style={{ height: `${(itm.prc / 100) * 12}px` }} />
)}
</span>
))}
</div>
</DialogContent>
</div>
)
}
function getCache(hash, callback) {
try {
fetch(cacheHost(), {
method: 'post',
body: JSON.stringify({ action: 'get', hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(
json => {
callback(json)
},
error => {
callback({})
console.error(error)
},
)
} catch (e) {
console.error(e)
callback({})
}
}
/*
{
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
"Capacity": 209715200,
"Filled": 2914808,
"PiecesLength": 4194304,
"PiecesCount": 2065,
"DownloadSpeed": 32770.860273455524,
"Pieces": {
"2064": {
"Id": 2064,
"Length": 2914808,
"Size": 162296,
"Completed": false
}
}
}
*/

View File

@@ -0,0 +1,45 @@
import { SectionTitle, WidgetWrapper } from '../style'
import { DetailedViewCacheSection, DetailedViewWidgetSection } from './style'
import TorrentCache from '../TorrentCache'
import {
SizeWidget,
PiecesLengthWidget,
StatusWidget,
PiecesCountWidget,
PeersWidget,
UploadSpeedWidget,
DownlodSpeedWidget,
} from '../widgets'
export default function Test({
downloadSpeed,
uploadSpeed,
torrent,
torrentSize,
PiecesCount,
PiecesLength,
statString,
cache,
}) {
return (
<>
<DetailedViewWidgetSection>
<SectionTitle mb={20}>Data</SectionTitle>
<WidgetWrapper detailedView>
<DownlodSpeedWidget data={downloadSpeed} />
<UploadSpeedWidget data={uploadSpeed} />
<PeersWidget data={torrent} />
<SizeWidget data={torrentSize} />
<PiecesCountWidget data={PiecesCount} />
<PiecesLengthWidget data={PiecesLength} />
<StatusWidget data={statString} />
</WidgetWrapper>
</DetailedViewWidgetSection>
<DetailedViewCacheSection>
<SectionTitle mb={20}>Cache</SectionTitle>
<TorrentCache cache={cache} />
</DetailedViewCacheSection>
</>
)
}

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components'
export const DetailedViewWidgetSection = styled.section`
padding: 40px;
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
@media (max-width: 800px) {
padding: 20px;
}
`
export const DetailedViewCacheSection = styled.section`
padding: 40px;
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
@media (max-width: 800px) {
padding: 20px;
}
`

View File

@@ -0,0 +1,33 @@
import Button from '@material-ui/core/Button'
import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core'
import CloseIcon from '@material-ui/icons/Close'
import { ArrowBack } from '@material-ui/icons'
const useStyles = makeStyles(theme => ({
appBar: { position: 'relative' },
title: { marginLeft: theme.spacing(2), flex: 1 },
}))
export default function DialogHeader({ title, onClose, onBack }) {
const classes = useStyles()
return (
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton edge='start' color='inherit' onClick={onBack || onClose} aria-label='close'>
{onBack ? <ArrowBack /> : <CloseIcon />}
</IconButton>
<Typography variant='h6' className={classes.title}>
{title}
</Typography>
{onBack && (
<Button autoFocus color='inherit' onClick={onClose}>
close
</Button>
)}
</Toolbar>
</AppBar>
)
}

View File

@@ -0,0 +1,53 @@
import { Rect } from 'react-konva'
export default function SingleBlock({
x,
y,
percentage,
isActive = false,
inProgress = false,
isReaderRange = false,
isComplete = false,
boxHeight,
strokeWidth,
}) {
const strokeColor = isActive
? '#000'
: isComplete
? '#3fb57a'
: inProgress
? '#00d0d0'
: isReaderRange
? '#9a9aff'
: '#eef2f4'
const backgroundColor = inProgress ? '#00d0d0' : '#eef2f4'
const percentageProgressColor = '#3fb57a'
const processCompletedColor = '#3fb57a'
return (
<Rect
x={x}
y={y}
stroke={strokeColor}
strokeWidth={strokeWidth}
height={boxHeight}
width={boxHeight}
fillAfterStrokeEnabled
preventDefault={false}
{...(isComplete
? { fill: processCompletedColor }
: inProgress && {
fillLinearGradientStartPointY: boxHeight,
fillLinearGradientEndPointY: 0,
fillLinearGradientColorStops: [
0,
percentageProgressColor,
percentage,
percentageProgressColor,
percentage,
backgroundColor,
],
})}
/>
)
}

View File

@@ -0,0 +1,14 @@
import { WidgetFieldWrapper, WidgetFieldIcon, WidgetFieldValue, WidgetFieldTitle } from './style'
export default function StatisticsField({ icon: Icon, title, value, iconBg, valueBg }) {
return (
<WidgetFieldWrapper>
<WidgetFieldTitle>{title}</WidgetFieldTitle>
<WidgetFieldIcon bgColor={iconBg}>
<Icon />
</WidgetFieldIcon>
<WidgetFieldValue bgColor={valueBg}>{value}</WidgetFieldValue>
</WidgetFieldWrapper>
)
}

View File

@@ -0,0 +1,147 @@
import { streamHost } from 'utils/Hosts'
import { isEqual } from 'lodash'
import { humanizeSize } from 'utils/Utils'
import ptt from 'parse-torrent-title'
import { Button } from '@material-ui/core'
import CopyToClipboard from 'react-copy-to-clipboard'
import { TableStyle, ShortTableWrapper, ShortTable } from './style'
const { memo } = require('react')
const Table = memo(
({ playableFileList, viewedFileList, selectedSeason, seasonAmount, hash }) => {
const preloadBuffer = fileId => fetch(`${streamHost()}?link=${hash}&index=${fileId}&preload`)
const getFileLink = (path, id) =>
`${streamHost()}/${encodeURIComponent(path.split('\\').pop().split('/').pop())}?link=${hash}&index=${id}&play`
const fileHasEpisodeText = !!playableFileList?.find(({ path }) => ptt.parse(path).episode)
const fileHasSeasonText = !!playableFileList?.find(({ path }) => ptt.parse(path).season)
const fileHasResolutionText = !!playableFileList?.find(({ path }) => ptt.parse(path).resolution)
return !playableFileList?.length ? (
'No playable files in this torrent'
) : (
<>
<TableStyle>
<thead>
<tr>
<th style={{ width: '0' }}>viewed</th>
<th>name</th>
{fileHasSeasonText && seasonAmount?.length === 1 && <th style={{ width: '0' }}>season</th>}
{fileHasEpisodeText && <th style={{ width: '0' }}>episode</th>}
{fileHasResolutionText && <th style={{ width: '0' }}>resolution</th>}
<th style={{ width: '100px' }}>size</th>
<th style={{ width: '400px' }}>actions</th>
</tr>
</thead>
<tbody>
{playableFileList.map(({ id, path, length }) => {
const { title, resolution, episode, season } = ptt.parse(path)
const isViewed = viewedFileList?.includes(id)
const link = getFileLink(path, id)
return (
(season === selectedSeason || !seasonAmount?.length) && (
<tr key={id} className={isViewed ? 'viewed-file-row' : null}>
<td data-label='viewed' className={isViewed ? 'viewed-file-indicator' : null} />
<td data-label='name'>{title}</td>
{fileHasSeasonText && seasonAmount?.length === 1 && <td data-label='season'>{season}</td>}
{fileHasEpisodeText && <td data-label='episode'>{episode}</td>}
{fileHasResolutionText && <td data-label='resolution'>{resolution}</td>}
<td data-label='size'>{humanizeSize(length)}</td>
<td className='button-cell'>
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
Preload
</Button>
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
Open link
</Button>
</a>
<CopyToClipboard text={link}>
<Button variant='outlined' color='primary' size='small'>
Copy link
</Button>
</CopyToClipboard>
</td>
</tr>
)
)
})}
</tbody>
</TableStyle>
<ShortTableWrapper>
{playableFileList.map(({ id, path, length }) => {
const { title, resolution, episode, season } = ptt.parse(path)
const isViewed = viewedFileList?.includes(id)
const link = getFileLink(path, id)
return (
(season === selectedSeason || !seasonAmount?.length) && (
<ShortTable key={id} isViewed={isViewed}>
<div className='short-table-name'>{title}</div>
<div className='short-table-data'>
{isViewed && (
<div className='short-table-field'>
<div className='short-table-field-name'>viewed</div>
<div className='short-table-field-value'>
<div className='short-table-viewed-indicator' />
</div>
</div>
)}
{fileHasSeasonText && seasonAmount?.length === 1 && (
<div className='short-table-field'>
<div className='short-table-field-name'>season</div>
<div className='short-table-field-value'>{season}</div>
</div>
)}
{fileHasEpisodeText && (
<div className='short-table-field'>
<div className='short-table-field-name'>epoisode</div>
<div className='short-table-field-value'>{episode}</div>
</div>
)}
{fileHasResolutionText && (
<div className='short-table-field'>
<div className='short-table-field-name'>resolution</div>
<div className='short-table-field-value'>{resolution}</div>
</div>
)}
<div className='short-table-field'>
<div className='short-table-field-name'>size</div>
<div className='short-table-field-value'>{humanizeSize(length)}</div>
</div>
</div>
<div className='short-table-buttons'>
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
Preload
</Button>
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
Open link
</Button>
</a>
<CopyToClipboard text={link}>
<Button variant='outlined' color='primary' size='small'>
Copy link
</Button>
</CopyToClipboard>
</div>
</ShortTable>
)
)
})}
</ShortTableWrapper>
</>
)
},
(prev, next) => isEqual(prev, next),
)
export default Table

View File

@@ -0,0 +1,172 @@
import styled, { css } from 'styled-components'
const viewedIndicator = css`
:before {
content: '';
width: 10px;
height: 10px;
background: #15d5af;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`
export const TableStyle = styled.table`
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
width: 100%;
border-radius: 5px 5px 0 0;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
thead tr {
background: #009879;
color: #fff;
text-align: left;
text-transform: uppercase;
}
th,
td {
padding: 12px 15px;
}
tbody tr {
border-bottom: 1px solid #ddd;
:last-of-type {
border-bottom: 2px solid #009879;
}
&.viewed-file-row {
background: #f3f3f3;
}
}
td {
&.viewed-file-indicator {
position: relative;
${viewedIndicator}
}
&.button-cell {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
}
@media (max-width: 970px) {
display: none;
}
`
export const ShortTableWrapper = styled.div`
display: grid;
gap: 20px;
grid-template-columns: repeat(2, 1fr);
display: none;
@media (max-width: 970px) {
display: grid;
}
@media (max-width: 820px) {
gap: 15px;
grid-template-columns: 1fr;
}
`
export const ShortTable = styled.div`
${({ isViewed }) => css`
width: 100%;
grid-template-rows: repeat(3, max-content);
border-radius: 5px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
.short-table {
&-name {
background: ${isViewed ? '#bdbdbd' : '#009879'};
display: grid;
place-items: center;
padding: 15px;
color: #fff;
text-transform: uppercase;
font-size: 15px;
font-weight: bold;
@media (max-width: 880px) {
font-size: 13px;
padding: 10px;
}
}
&-data {
display: grid;
grid-auto-flow: column;
grid-template-columns: ${isViewed ? 'max-content' : '1fr'};
grid-auto-columns: 1fr;
}
&-field {
display: grid;
grid-template-rows: 30px 1fr;
background: black;
:not(:last-child) {
border-right: 1px solid ${isViewed ? '#bdbdbd' : '#019376'};
}
&-name {
background: ${isViewed ? '#c4c4c4' : '#00a383'};
color: #fff;
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
display: grid;
place-items: center;
padding: 0 10px;
@media (max-width: 880px) {
font-size: 11px;
}
}
&-value {
background: ${isViewed ? '#c9c9c9' : '#03aa89'};
display: grid;
place-items: center;
color: #fff;
font-size: 15px;
padding: 15px 10px;
position: relative;
@media (max-width: 880px) {
font-size: 13px;
padding: 12px 8px;
}
}
}
&-viewed-indicator {
${isViewed && viewedIndicator}
}
&-buttons {
padding: 20px;
border-bottom: 2px solid ${isViewed ? '#bdbdbd' : '#009879'};
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
gap: 20px;
@media (max-width: 410px) {
gap: 10px;
grid-template-columns: 1fr;
}
}
}
`}
`

View File

@@ -0,0 +1,110 @@
import { memo, useEffect, useState } from 'react'
import DialogContent from '@material-ui/core/DialogContent'
import { Stage, Layer } from 'react-konva'
import Measure from 'react-measure'
import { isEqual } from 'lodash'
import SingleBlock from './SingleBlock'
import { useCreateCacheMap } from './customHooks'
const TorrentCache = memo(
({ cache, isMini }) => {
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
const [stageSettings, setStageSettings] = useState({
boxHeight: null,
strokeWidth: null,
marginBetweenBlocks: null,
stageOffset: null,
})
const cacheMap = useCreateCacheMap(cache)
const updateStageSettings = (boxHeight, strokeWidth) => {
setStageSettings({
boxHeight,
strokeWidth,
marginBetweenBlocks: strokeWidth,
stageOffset: strokeWidth * 2,
})
}
useEffect(() => {
// initializing stageSettings
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
updateStageSettings(12, 2)
}, [isMini, dimensions.width])
const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks
const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin)
const amountOfBlocksToRenderInShortView =
preloadPiecesAmount === piecesInOneRow
? preloadPiecesAmount - 1
: preloadPiecesAmount + piecesInOneRow - (preloadPiecesAmount % piecesInOneRow) - 1
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
let activeId = null
return (
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
{({ measureRef }) => (
<div ref={measureRef}>
<DialogContent style={{ padding: 0 }}>
<Stage
style={{ display: 'flex', justifyContent: 'center' }}
offset={{ x: -stageOffset, y: -stageOffset }}
width={stageOffset + blockSizeWithMargin * piecesInOneRow || 0}
height={stageOffset + blockSizeWithMargin * amountOfRows || 0}
>
<Layer>
{cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow)
// -------- related only for short view -------
if (isActive) activeId = id
const shouldBeRendered =
isActive || (id - activeId <= amountOfBlocksToRenderInShortView && id - activeId >= 0)
// --------------------------------------------
return isMini ? (
shouldBeRendered && (
<SingleBlock
key={id}
x={((id - activeId) % piecesInOneRow) * blockSizeWithMargin}
y={currentRow * blockSizeWithMargin}
percentage={percentage}
inProgress={inProgress}
isComplete={isComplete}
isReaderRange={isReaderRange}
isActive={isActive}
boxHeight={boxHeight}
strokeWidth={strokeWidth}
/>
)
) : (
<SingleBlock
key={id}
x={(id % piecesInOneRow) * blockSizeWithMargin}
y={currentRow * blockSizeWithMargin}
percentage={percentage}
inProgress={inProgress}
isComplete={isComplete}
isReaderRange={isReaderRange}
isActive={isActive}
boxHeight={boxHeight}
strokeWidth={strokeWidth}
/>
)
})}
</Layer>
</Stage>
</DialogContent>
</div>
)}
</Measure>
)
},
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
)
export default TorrentCache

View File

@@ -0,0 +1,83 @@
import axios from 'axios'
import { memo } from 'react'
import { playlistTorrHost, torrentsHost, viewedHost } from 'utils/Hosts'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { Button } from '@material-ui/core'
import ptt from 'parse-torrent-title'
import { SmallLabel, MainSectionButtonGroup } from './style'
import { SectionSubName } from '../style'
const TorrentFunctions = memo(
({ hash, viewedFileList, playableFileList, name, title, setViewedFileList }) => {
const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1]
const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path
const isOnlyOnePlayableFile = playableFileList?.length === 1
const latestViewedFileData = latestViewedFile && ptt.parse(latestViewedFile)
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
const removeTorrentViews = () =>
axios.post(viewedHost(), { action: 'rem', hash, file_index: -1 }).then(() => setViewedFileList())
const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(name || title || 'file')}.m3u?link=${hash}&m3u`
const partialPlaylistLink = `${fullPlaylistLink}&fromlast`
return (
<>
{!isOnlyOnePlayableFile && !!viewedFileList?.length && (
<>
<SmallLabel>Download Playlist</SmallLabel>
<SectionSubName mb={10}>
<strong>Latest file played:</strong> {latestViewedFileData?.title}.
{latestViewedFileData?.season && (
<>
{' '}
Season: {latestViewedFileData?.season}. Episode: {latestViewedFileData?.episode}.
</>
)}
</SectionSubName>
<MainSectionButtonGroup>
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
full
</Button>
</a>
<a style={{ textDecoration: 'none' }} href={partialPlaylistLink}>
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
from latest file
</Button>
</a>
</MainSectionButtonGroup>
</>
)}
<SmallLabel mb={10}>Torrent State</SmallLabel>
<MainSectionButtonGroup>
<Button onClick={() => removeTorrentViews()} variant='contained' color='primary' size='large'>
remove views
</Button>
<Button onClick={() => dropTorrent()} variant='contained' color='primary' size='large'>
drop torrent
</Button>
</MainSectionButtonGroup>
<SmallLabel mb={10}>Info</SmallLabel>
<MainSectionButtonGroup>
{(isOnlyOnePlayableFile || !viewedFileList?.length) && (
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
download playlist
</Button>
</a>
)}
<CopyToClipboard text={hash}>
<Button variant='contained' color='primary' size='large'>
copy hash
</Button>
</CopyToClipboard>
</MainSectionButtonGroup>
</>
)
},
() => true,
)
export default TorrentFunctions

View File

@@ -0,0 +1,33 @@
import styled, { css } from 'styled-components'
export const MainSectionButtonGroup = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
:not(:last-child) {
margin-bottom: 30px;
}
@media (max-width: 1580px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 880px) {
grid-template-columns: 1fr;
}
`
export const SmallLabel = styled.div`
${({ mb }) => css`
${mb && `margin-bottom: ${mb}px`};
font-size: 20px;
font-weight: 300;
line-height: 1;
@media (max-width: 800px) {
font-size: 18px;
${mb && `margin-bottom: ${mb / 1.5}px`};
}
`}
`

View File

@@ -0,0 +1,80 @@
import { useEffect, useRef, useState } from 'react'
import { cacheHost, settingsHost } from 'utils/Hosts'
import axios from 'axios'
export const useUpdateCache = hash => {
const [cache, setCache] = useState({})
const componentIsMounted = useRef(true)
const timerID = useRef(null)
useEffect(
() => () => {
// this function is required to notify "updateCache" when NOT to make state update
componentIsMounted.current = false
},
[],
)
useEffect(() => {
if (hash) {
timerID.current = setInterval(() => {
const updateCache = newCache => componentIsMounted.current && setCache(newCache)
axios
.post(cacheHost(), { action: 'get', hash })
.then(({ data }) => updateCache(data))
// empty cache if error
.catch(() => updateCache({}))
}, 100)
} else clearInterval(timerID.current)
return () => clearInterval(timerID.current)
}, [hash])
return cache
}
export const useCreateCacheMap = cache => {
const [cacheMap, setCacheMap] = useState([])
useEffect(() => {
if (!cache.PiecesCount || !cache.Pieces) return
const { Pieces, PiecesCount, Readers } = cache
const map = []
for (let i = 0; i < PiecesCount; i++) {
const newPiece = { id: i }
const currentPiece = Pieces[i]
if (currentPiece) {
if (currentPiece.Completed && currentPiece.Size === currentPiece.Length) newPiece.isComplete = true
else {
newPiece.inProgress = true
newPiece.percentage = (currentPiece.Size / currentPiece.Length).toFixed(2)
}
}
Readers.forEach(r => {
if (i === r.Reader) newPiece.isActive = true
if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true
})
map.push(newPiece)
}
setCacheMap(map)
}, [cache])
return cacheMap
}
export const useGetSettings = cache => {
const [settings, setSettings] = useState()
useEffect(() => {
axios.post(settingsHost(), { action: 'get' }).then(({ data }) => setSettings(data))
}, [cache])
return settings
}

View File

@@ -0,0 +1,68 @@
const getExt = filename => {
const ext = filename.split('.').pop()
if (ext === filename) return ''
return ext.toLowerCase()
}
const playableExtList = [
// video
'3g2',
'3gp',
'aaf',
'asf',
'avchd',
'avi',
'drc',
'flv',
'iso',
'm2v',
'm2ts',
'm4p',
'm4v',
'mkv',
'mng',
'mov',
'mp2',
'mp4',
'mpe',
'mpeg',
'mpg',
'mpv',
'mxf',
'nsv',
'ogg',
'ogv',
'ts',
'qt',
'rm',
'rmvb',
'roq',
'svi',
'vob',
'webm',
'wmv',
'yuv',
// audio
'aac',
'aiff',
'ape',
'au',
'flac',
'gsm',
'it',
'm3u',
'm4a',
'mid',
'mod',
'mp3',
'mpa',
'pls',
'ra',
's3m',
'sid',
'wav',
'wma',
'xm',
]
// eslint-disable-next-line import/prefer-default-export
export const isFilePlayable = fileName => playableExtList.includes(getExt(fileName))

View File

@@ -0,0 +1,231 @@
import { NoImageIcon } from 'icons'
import { humanizeSize, shortenText } from 'utils/Utils'
import { useEffect, useState } from 'react'
import { Button, ButtonGroup } from '@material-ui/core'
import ptt from 'parse-torrent-title'
import axios from 'axios'
import { viewedHost } from 'utils/Hosts'
import { GETTING_INFO, IN_DB } from 'torrentStates'
import CircularProgress from '@material-ui/core/CircularProgress'
import { useUpdateCache, useGetSettings } from './customHooks'
import DialogHeader from './DialogHeader'
import TorrentCache from './TorrentCache'
import Table from './Table'
import DetailedView from './DetailedView'
import {
DialogContentGrid,
MainSection,
Poster,
SectionTitle,
SectionSubName,
WidgetWrapper,
LoadingProgress,
SectionHeader,
CacheSection,
TorrentFilesSection,
Divider,
} from './style'
import { DownlodSpeedWidget, UploadSpeedWidget, PeersWidget, SizeWidget } from './widgets'
import TorrentFunctions from './TorrentFunctions'
import { isFilePlayable } from './helpers'
const Loader = () => (
<div style={{ minHeight: '80vh', display: 'grid', placeItems: 'center' }}>
<CircularProgress />
</div>
)
export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
const [isLoading, setIsLoading] = useState(true)
const [isDetailedCacheView, setIsDetailedCacheView] = useState(false)
const [viewedFileList, setViewedFileList] = useState()
const [playableFileList, setPlayableFileList] = useState()
const [seasonAmount, setSeasonAmount] = useState(null)
const [selectedSeason, setSelectedSeason] = useState()
const {
poster,
hash,
title,
name,
stat,
download_speed: downloadSpeed,
upload_speed: uploadSpeed,
stat_string: statString,
torrent_size: torrentSize,
file_stats: torrentFileList,
} = torrent
const cache = useUpdateCache(hash)
const settings = useGetSettings(cache)
const { Capacity, PiecesCount, PiecesLength, Filled } = cache
useEffect(() => {
if (playableFileList && seasonAmount === null) {
const seasons = []
playableFileList.forEach(({ path }) => {
const currentSeason = ptt.parse(path).season
if (currentSeason) {
!seasons.includes(currentSeason) && seasons.push(currentSeason)
}
})
seasons.length && setSelectedSeason(seasons[0])
setSeasonAmount(seasons.sort((a, b) => a - b))
}
}, [playableFileList, seasonAmount])
useEffect(() => {
setPlayableFileList(torrentFileList?.filter(({ path }) => isFilePlayable(path)))
}, [torrentFileList])
useEffect(() => {
const cacheLoaded = !!Object.entries(cache).length
const torrentLoaded = stat !== GETTING_INFO && stat !== IN_DB
if (!cacheLoaded && !isLoading) setIsLoading(true)
if (cacheLoaded && isLoading && torrentLoaded) setIsLoading(false)
}, [stat, cache, isLoading])
useEffect(() => {
// getting viewed file list
axios.post(viewedHost(), { action: 'list', hash }).then(({ data }) => {
if (data) {
const lst = data.map(itm => itm.file_index).sort((a, b) => a - b)
setViewedFileList(lst)
} else setViewedFileList()
})
}, [hash])
const bufferSize = settings?.PreloadBuffer ? Capacity : 33554432 // Default is 32mb if PreloadBuffer is false
const getTitle = value => {
const torrentParsedName = value && ptt.parse(value)
const newNameStrings = []
if (torrentParsedName?.title) newNameStrings.push(` ${torrentParsedName?.title}`)
if (torrentParsedName?.year) newNameStrings.push(`. ${torrentParsedName?.year}.`)
if (torrentParsedName?.resolution) newNameStrings.push(` (${torrentParsedName?.resolution})`)
return newNameStrings.join(' ')
}
return (
<>
<DialogHeader
onClose={closeDialog}
title={isDetailedCacheView ? 'Detailed Cache View' : 'Torrent Details'}
{...(isDetailedCacheView && { onBack: () => setIsDetailedCacheView(false) })}
/>
<div style={{ minHeight: '80vh', overflow: 'auto' }}>
{isLoading ? (
<Loader />
) : isDetailedCacheView ? (
<DetailedView
downloadSpeed={downloadSpeed}
uploadSpeed={uploadSpeed}
torrent={torrent}
torrentSize={torrentSize}
PiecesCount={PiecesCount}
PiecesLength={PiecesLength}
statString={statString}
cache={cache}
/>
) : (
<DialogContentGrid>
<MainSection>
<Poster poster={poster}>{poster ? <img alt='poster' src={poster} /> : <NoImageIcon />}</Poster>
<div>
{name && name !== title ? (
<>
<SectionTitle>{shortenText(getTitle(name), 50)}</SectionTitle>
<SectionSubName mb={20}>{shortenText(title, 160)}</SectionSubName>
</>
) : (
<SectionTitle mb={20}>{shortenText(getTitle(title), 50)}</SectionTitle>
)}
<WidgetWrapper>
<DownlodSpeedWidget data={downloadSpeed} />
<UploadSpeedWidget data={uploadSpeed} />
<PeersWidget data={torrent} />
<SizeWidget data={torrentSize} />
</WidgetWrapper>
<Divider />
<TorrentFunctions
hash={hash}
viewedFileList={viewedFileList}
playableFileList={playableFileList}
name={name}
title={title}
setViewedFileList={setViewedFileList}
/>
</div>
</MainSection>
<CacheSection>
<SectionHeader>
<SectionTitle mb={20}>Buffer</SectionTitle>
{!settings?.PreloadBuffer && (
<SectionSubName>Enable &quot;Preload Buffer&quot; in settings to change buffer size</SectionSubName>
)}
<LoadingProgress
value={Filled}
fullAmount={bufferSize}
label={`${humanizeSize(Filled) || '0 B'} / ${humanizeSize(bufferSize)}`}
/>
</SectionHeader>
<TorrentCache isMini cache={cache} />
<Button
style={{ marginTop: '30px' }}
variant='contained'
color='primary'
size='large'
onClick={() => setIsDetailedCacheView(true)}
>
Detailed cache view
</Button>
</CacheSection>
<TorrentFilesSection>
<SectionTitle mb={20}>Torrent Content</SectionTitle>
{seasonAmount?.length > 1 && (
<>
<SectionSubName mb={7}>Select Season</SectionSubName>
<ButtonGroup style={{ marginBottom: '30px' }} color='primary'>
{seasonAmount.map(season => (
<Button
key={season}
variant={selectedSeason === season ? 'contained' : 'outlined'}
onClick={() => setSelectedSeason(season)}
>
{season}
</Button>
))}
</ButtonGroup>
<SectionTitle mb={20}>Season {selectedSeason}</SectionTitle>
</>
)}
<Table
hash={hash}
playableFileList={playableFileList}
viewedFileList={viewedFileList}
selectedSeason={selectedSeason}
seasonAmount={seasonAmount}
/>
</TorrentFilesSection>
</DialogContentGrid>
)}
</div>
</>
)
}

View File

@@ -0,0 +1,253 @@
import styled, { css } from 'styled-components'
export const DialogContentGrid = styled.div`
display: grid;
grid-template-columns: 70% 1fr;
grid-template-rows: repeat(2, min-content);
grid-template-areas:
'main cache'
'file-list file-list';
@media (max-width: 1450px) {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, min-content);
grid-template-areas:
'main'
'cache'
'file-list';
}
`
export const Poster = styled.div`
${({ poster }) => css`
height: 400px;
border-radius: 5px;
overflow: hidden;
align-self: center;
${poster
? css`
img {
border-radius: 5px;
height: 100%;
}
`
: css`
width: 300px;
display: grid;
place-items: center;
background: #74c39c;
svg {
transform: scale(2.5) translateY(-3px);
}
`}
@media (max-width: 1280px) {
align-self: start;
}
@media (max-width: 840px) {
height: 200px;
${!poster &&
css`
width: 150px;
svg {
transform: translateY(-3px);
}
`}
}
`}
`
export const MainSection = styled.section`
grid-area: main;
padding: 40px;
display: grid;
grid-template-columns: min-content 1fr;
gap: 30px;
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
@media (max-width: 840px) {
grid-template-columns: 1fr;
}
@media (max-width: 800px) {
padding: 20px;
}
`
export const CacheSection = styled.section`
grid-area: cache;
padding: 40px;
display: grid;
align-content: start;
grid-template-rows: min-content 1fr min-content;
background: #88cdaa;
@media (max-width: 800px) {
padding: 20px;
}
`
export const TorrentFilesSection = styled.section`
grid-area: file-list;
padding: 40px;
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
@media (max-width: 800px) {
padding: 20px;
}
`
export const SectionSubName = styled.div`
${({ mb }) => css`
${mb && `margin-bottom: ${mb}px`};
color: #7c7b7c;
@media (max-width: 800px) {
${mb && `margin-bottom: ${mb / 2}px`};
font-size: 11px;
}
`}
`
export const SectionTitle = styled.div`
${({ mb }) => css`
${mb && `margin-bottom: ${mb}px`};
font-size: 35px;
font-weight: 200;
line-height: 1;
word-break: break-word;
@media (max-width: 800px) {
font-size: 25px;
${mb && `margin-bottom: ${mb / 2}px`};
}
`}
`
export const SectionHeader = styled.div`
margin-bottom: 20px;
`
export const WidgetWrapper = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(max-content, 220px));
gap: 20px;
@media (max-width: 800px) {
gap: 15px;
}
@media (max-width: 410px) {
gap: 10px;
}
${({ detailedView }) =>
detailedView
? css`
@media (max-width: 800px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 410px) {
grid-template-columns: 1fr;
}
`
: css`
@media (max-width: 800px) {
grid-template-columns: repeat(auto-fit, minmax(max-content, 185px));
}
@media (max-width: 480px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 390px) {
grid-template-columns: 1fr;
}
`}
`
export const WidgetFieldWrapper = styled.div`
display: grid;
grid-template-columns: 40px 1fr;
grid-template-rows: min-content 50px;
grid-template-areas:
'title title'
'icon value';
> * {
display: grid;
place-items: center;
}
@media (max-width: 800px) {
grid-template-columns: 30px 1fr;
grid-template-rows: min-content 40px;
}
`
export const WidgetFieldTitle = styled.div`
grid-area: title;
justify-self: start;
text-transform: uppercase;
font-size: 11px;
margin-bottom: 2px;
font-weight: 500;
`
export const WidgetFieldIcon = styled.div`
${({ bgColor }) => css`
grid-area: icon;
color: rgba(255, 255, 255, 0.8);
background: ${bgColor};
border-radius: 5px 0 0 5px;
@media (max-width: 800px) {
> svg {
width: 50%;
}
}
`}
`
export const WidgetFieldValue = styled.div`
${({ bgColor }) => css`
grid-area: value;
padding: 0 20px;
color: #fff;
font-size: 25px;
background: ${bgColor};
border-radius: 0 5px 5px 0;
@media (max-width: 800px) {
font-size: 18px;
padding: 0 4px;
}
`}
`
export const LoadingProgress = styled.div.attrs(({ value, fullAmount }) => {
const percentage = Math.min(100, (value * 100) / fullAmount)
return {
// this block is here according to styled-components recomendation about fast changable components
style: {
background: `linear-gradient(to right, #b5dec9 0%, #b5dec9 ${percentage}%, #fff ${percentage}%, #fff 100%)`,
},
}
})`
${({ label }) => css`
border: 1px solid;
padding: 10px 20px;
border-radius: 5px;
:before {
content: '${label}';
display: grid;
place-items: center;
font-size: 20px;
}
`}
`
export const Divider = styled.div`
height: 1px;
background-color: rgba(0, 0, 0, 0.12);
margin: 30px 0;
`

View File

@@ -0,0 +1,68 @@
import {
ArrowDownward as ArrowDownwardIcon,
ArrowUpward as ArrowUpwardIcon,
SwapVerticalCircle as SwapVerticalCircleIcon,
ViewAgenda as ViewAgendaIcon,
Widgets as WidgetsIcon,
PhotoSizeSelectSmall as PhotoSizeSelectSmallIcon,
Build as BuildIcon,
} from '@material-ui/icons'
import { getPeerString, humanizeSize } from 'utils/Utils'
import StatisticsField from './StatisticsField'
export const DownlodSpeedWidget = ({ data }) => (
<StatisticsField
title='Download speed'
value={humanizeSize(data) || '0 B'}
iconBg='#118f00'
valueBg='#13a300'
icon={ArrowDownwardIcon}
/>
)
export const UploadSpeedWidget = ({ data }) => (
<StatisticsField
title='Upload speed'
value={humanizeSize(data) || '0 B'}
iconBg='#0146ad'
valueBg='#0058db'
icon={ArrowUpwardIcon}
/>
)
export const PeersWidget = ({ data }) => (
<StatisticsField
title='Peers'
value={getPeerString(data)}
iconBg='#cdc118'
valueBg='#d8cb18'
icon={SwapVerticalCircleIcon}
/>
)
export const PiecesCountWidget = ({ data }) => (
<StatisticsField title='Pieces count' value={data} iconBg='#b6c95e' valueBg='#c0d076' icon={WidgetsIcon} />
)
export const PiecesLengthWidget = ({ data }) => (
<StatisticsField
title='Pieces length'
value={humanizeSize(data)}
iconBg='#0982c8'
valueBg='#098cd7'
icon={PhotoSizeSelectSmallIcon}
/>
)
export const StatusWidget = ({ data }) => (
<StatisticsField title='Torrent status' value={data} iconBg='#aea25b' valueBg='#b4aa6e' icon={BuildIcon} />
)
export const SizeWidget = ({ data }) => (
<StatisticsField
title='Torrent size'
value={humanizeSize(data)}
iconBg='#9b01ad'
valueBg='#ac03bf'
icon={ViewAgendaIcon}
/>
)

View File

@@ -1,254 +0,0 @@
import { useEffect, useState } from 'react'
import Typography from '@material-ui/core/Typography'
import { Button, ButtonGroup, Grid, List, ListItem } from '@material-ui/core'
import CachedIcon from '@material-ui/icons/Cached'
import LinearProgress from '@material-ui/core/LinearProgress'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import { getPeerString, humanizeSize } from 'utils/Utils'
import { playlistTorrHost, streamHost, viewedHost } from 'utils/Hosts'
const style = {
width100: {
width: '100%',
},
width80: {
width: '80%',
},
poster: {
display: 'flex',
flexDirection: 'row',
borderRadius: '5px',
},
}
export default function DialogTorrentInfo({ torrent, open }) {
const [torrentLocalComponentValue, setTorrentLocalComponentValue] = useState(torrent)
const [viewed, setViewed] = useState(null)
const [progress, setProgress] = useState(-1)
useEffect(() => {
setTorrentLocalComponentValue(torrent)
if (torrentLocalComponentValue.stat === 2)
setProgress((torrentLocalComponentValue.preloaded_bytes * 100) / torrentLocalComponentValue.preload_size)
getViewed(torrent.hash, list => {
if (list) {
const lst = list.map(itm => itm.file_index)
setViewed(lst)
} else setViewed(null)
})
}, [torrent, open])
return (
<div>
<DialogTitle id='form-dialog-title'>
<Grid container spacing={1}>
<Grid item>
{torrentLocalComponentValue.poster && (
<img alt='' height='200' align='left' style={style.poster} src={torrentLocalComponentValue.poster} />
)}
</Grid>
<Grid style={style.width80} item>
{torrentLocalComponentValue.title}{' '}
{torrentLocalComponentValue.name &&
torrentLocalComponentValue.name !== torrentLocalComponentValue.title &&
` | ${torrentLocalComponentValue.name}`}
<Typography>
<b>Peers: </b> {getPeerString(torrentLocalComponentValue)}
<br />
<b>Loaded: </b> {getPreload(torrentLocalComponentValue)}
<br />
<b>Speed: </b> {humanizeSize(torrentLocalComponentValue.download_speed)}
<br />
<b>Status: </b> {torrentLocalComponentValue.stat_string}
<br />
</Typography>
</Grid>
</Grid>
{torrentLocalComponentValue.stat === 2 && (
<LinearProgress style={{ marginTop: '10px' }} variant='determinate' value={progress} />
)}
</DialogTitle>
<DialogContent>
<List>
<ListItem key='TorrentMenu'>
<ButtonGroup
style={style.width100}
variant='contained'
color='primary'
aria-label='contained primary button group'
>
<Button
style={style.width100}
href={`${playlistTorrHost()}/${encodeURIComponent(
torrentLocalComponentValue.name || torrentLocalComponentValue.title || 'file',
)}.m3u?link=${torrentLocalComponentValue.hash}&m3u`}
>
Playlist
</Button>
<Button
style={style.width100}
href={`${playlistTorrHost()}/${encodeURIComponent(
torrentLocalComponentValue.name || torrentLocalComponentValue.title || 'file',
)}.m3u?link=${torrentLocalComponentValue.hash}&m3u&fromlast`}
>
Playlist after last view
</Button>
<Button
style={style.width100}
onClick={() => {
remViews(torrentLocalComponentValue.hash)
setViewed(null)
}}
>
Remove views
</Button>
</ButtonGroup>
</ListItem>
{getPlayableFile(torrentLocalComponentValue) &&
getPlayableFile(torrentLocalComponentValue).map(file => (
<ButtonGroup style={style.width100} disableElevation variant='contained' color='primary'>
<Button
style={style.width100}
href={`${streamHost()}/${encodeURIComponent(file.path.split('\\').pop().split('/').pop())}?link=${
torrentLocalComponentValue.hash
}&index=${file.id}&play`}
>
<Typography>
{file.path.split('\\').pop().split('/').pop()} | {humanizeSize(file.length)}{' '}
{viewed && viewed.indexOf(file.id) !== -1 && '| ✓'}
</Typography>
</Button>
<Button
onClick={() =>
fetch(`${streamHost()}?link=${torrentLocalComponentValue.hash}&index=${file.id}&preload`)
}
>
<CachedIcon />
<Typography>Preload</Typography>
</Button>
</ButtonGroup>
))}
</List>
</DialogContent>
</div>
)
}
function remViews(hash) {
try {
if (hash)
fetch(viewedHost(), {
method: 'post',
body: JSON.stringify({ action: 'rem', hash, file_index: -1 }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
} catch (e) {
console.error(e)
}
}
function getViewed(hash, callback) {
try {
fetch(viewedHost(), {
method: 'post',
body: JSON.stringify({ action: 'list', hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(callback)
} catch (e) {
console.error(e)
}
}
function getPlayableFile(torrent) {
if (!torrent || !torrent.file_stats) return null
return torrent.file_stats.filter(file => extPlayable.includes(getExt(file.path)))
}
function getExt(filename) {
const ext = filename.split('.').pop()
if (ext === filename) return ''
return ext.toLowerCase()
}
function getPreload(torrent) {
if (torrent.preloaded_bytes > 0 && torrent.preload_size > 0 && torrent.preloaded_bytes < torrent.preload_size) {
const progress = ((torrent.preloaded_bytes * 100) / torrent.preload_size).toFixed(2)
return `${humanizeSize(torrent.preloaded_bytes)} / ${humanizeSize(torrent.preload_size)} ${progress}%`
}
if (!torrent.preloaded_bytes) return humanizeSize(0)
return humanizeSize(torrent.preloaded_bytes)
}
const extPlayable = [
// video
'3g2',
'3gp',
'aaf',
'asf',
'avchd',
'avi',
'drc',
'flv',
'iso',
'm2v',
'm2ts',
'm4p',
'm4v',
'mkv',
'mng',
'mov',
'mp2',
'mp4',
'mpe',
'mpeg',
'mpg',
'mpv',
'mxf',
'nsv',
'ogg',
'ogv',
'ts',
'qt',
'rm',
'rmvb',
'roq',
'svi',
'vob',
'webm',
'wmv',
'yuv',
// audio
'aac',
'aiff',
'ape',
'au',
'flac',
'gsm',
'it',
'm3u',
'm4a',
'mid',
'mod',
'mp3',
'mpa',
'pls',
'ra',
's3m',
'sid',
'wav',
'wma',
'xm',
]

View File

@@ -1,7 +1,9 @@
import { Button, Dialog, DialogActions, DialogTitle } from '@material-ui/core'
import ListItem from '@material-ui/core/ListItem' import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText' import ListItemText from '@material-ui/core/ListItemText'
import DeleteIcon from '@material-ui/icons/Delete' import DeleteIcon from '@material-ui/icons/Delete'
import { useState } from 'react'
import { torrentsHost } from 'utils/Hosts' import { torrentsHost } from 'utils/Hosts'
const fnRemoveAll = () => { const fnRemoveAll = () => {
@@ -29,13 +31,40 @@ const fnRemoveAll = () => {
} }
export default function RemoveAll() { export default function RemoveAll() {
return ( const [open, setOpen] = useState(false)
<ListItem button key='Remove all' onClick={fnRemoveAll}> const closeDialog = () => setOpen(false)
<ListItemIcon> const openDialog = () => setOpen(true)
<DeleteIcon />
</ListItemIcon>
<ListItemText primary='Remove all' /> return (
</ListItem> <>
<ListItem button key='Remove all' onClick={openDialog}>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText primary='Remove all' />
</ListItem>
<Dialog open={open} onClose={closeDialog}>
<DialogTitle>Delete Torrent?</DialogTitle>
<DialogActions>
<Button variant='outlined' onClick={closeDialog} color='primary'>
Cancel
</Button>
<Button
variant='contained'
onClick={() => {
fnRemoveAll()
closeDialog()
}}
color='primary'
autoFocus
>
Ok
</Button>
</DialogActions>
</Dialog>
</>
) )
} }

View File

@@ -26,7 +26,6 @@ export default function SettingsDialog() {
setOpen(false) setOpen(false)
const sets = JSON.parse(JSON.stringify(settings)) const sets = JSON.parse(JSON.stringify(settings))
sets.CacheSize *= 1024 * 1024 sets.CacheSize *= 1024 * 1024
sets.PreloadBufferSize *= 1024 * 1024
fetch(settingsHost(), { fetch(settingsHost(), {
method: 'post', method: 'post',
body: JSON.stringify({ action: 'set', sets }), body: JSON.stringify({ action: 'set', sets }),
@@ -51,8 +50,6 @@ export default function SettingsDialog() {
json => { json => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
json.CacheSize /= 1024 * 1024 json.CacheSize /= 1024 * 1024
// eslint-disable-next-line no-param-reassign
json.PreloadBufferSize /= 1024 * 1024
setSets(json) setSets(json)
setShow(true) setShow(true)
}, },
@@ -135,13 +132,7 @@ export default function SettingsDialog() {
<br /> <br />
<br /> <br />
<InputLabel htmlFor='RetrackersMode'>Retracker mode</InputLabel> <InputLabel htmlFor='RetrackersMode'>Retracker mode</InputLabel>
<Select <Select onChange={inputForm} type='number' native id='RetrackersMode' value={settings.RetrackersMode}>
onChange={inputForm}
type='number'
native='true'
id='RetrackersMode'
value={settings.RetrackersMode}
>
<option value={0}>Don&apos;t add retrackers</option> <option value={0}>Don&apos;t add retrackers</option>
<option value={1}>Add retrackers</option> <option value={1}>Add retrackers</option>
<option value={2}>Remove retrackers</option> <option value={2}>Remove retrackers</option>

View File

@@ -1,199 +0,0 @@
/* eslint-disable camelcase */
import 'fontsource-roboto'
import { useEffect, useRef, useState } from 'react'
import Button from '@material-ui/core/Button'
import HeightIcon from '@material-ui/icons/Height'
import CloseIcon from '@material-ui/icons/Close'
import DeleteIcon from '@material-ui/icons/Delete'
import DialogActions from '@material-ui/core/DialogActions'
import Dialog from '@material-ui/core/Dialog'
import DataUsageIcon from '@material-ui/icons/DataUsage'
import { getPeerString, humanizeSize } from 'utils/Utils'
import { torrentsHost } from 'utils/Hosts'
import { NoImageIcon } from 'icons'
import DialogTorrentInfo from 'components/DialogTorrentInfo'
import DialogCacheInfo from 'components/DialogCacheInfo'
import {
StyledButton,
TorrentCard,
TorrentCardButtons,
TorrentCardDescription,
TorrentCardDescriptionContent,
TorrentCardDescriptionLabel,
TorrentCardPoster,
TorrentCardDetails,
} from './style'
export default function Torrent({ torrent }) {
const [open, setOpen] = useState(false)
const [showCache, setShowCache] = useState(false)
const [torrentLocalComponentValue, setTorrentLocalComponentValue] = useState(torrent)
const timerID = useRef(-1)
useEffect(() => {
setTorrentLocalComponentValue(torrent)
}, [torrent])
useEffect(() => {
if (open)
timerID.current = setInterval(() => {
getTorrent(torrentLocalComponentValue.hash, (torr, error) => {
if (error) console.error(error)
else if (torr) setTorrentLocalComponentValue(torr)
})
}, 1000)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [torrentLocalComponentValue.hash, open])
const { title, name, poster, torrent_size, download_speed } = torrentLocalComponentValue
return (
<>
<TorrentCard>
<TorrentCardPoster isPoster={poster}>
{poster ? <img src={poster} alt='poster' /> : <NoImageIcon />}
</TorrentCardPoster>
<TorrentCardButtons>
<StyledButton
onClick={() => {
setShowCache(true)
setOpen(true)
}}
>
<DataUsageIcon />
<span>Cache</span>
</StyledButton>
<StyledButton onClick={() => dropTorrent(torrentLocalComponentValue)}>
<CloseIcon />
<span>Drop</span>
</StyledButton>
<StyledButton
onClick={() => {
setShowCache(false)
setOpen(true)
}}
>
<HeightIcon />
<span>Details</span>
</StyledButton>
<StyledButton onClick={() => deleteTorrent(torrentLocalComponentValue)}>
<DeleteIcon />
<span>Delete</span>
</StyledButton>
</TorrentCardButtons>
<TorrentCardDescription>
<span>
<TorrentCardDescriptionLabel>Name</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent isTitle>{title || name}</TorrentCardDescriptionContent>
</span>
<TorrentCardDetails>
<span>
<TorrentCardDescriptionLabel>Size</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>
{torrent_size > 0 && humanizeSize(torrent_size)}
</TorrentCardDescriptionContent>
</span>
<span>
<TorrentCardDescriptionLabel>Speed</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>
{download_speed > 0 ? humanizeSize(download_speed) : '---'}
</TorrentCardDescriptionContent>
</span>
<span>
<TorrentCardDescriptionLabel>Peers</TorrentCardDescriptionLabel>
<TorrentCardDescriptionContent>
{getPeerString(torrentLocalComponentValue) || '---'}
</TorrentCardDescriptionContent>
</span>
</TorrentCardDetails>
</TorrentCardDescription>
</TorrentCard>
<Dialog open={open} onClose={() => setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'>
{!showCache ? (
<DialogTorrentInfo torrent={(open, torrentLocalComponentValue)} />
) : (
<DialogCacheInfo hash={(open, torrentLocalComponentValue.hash)} />
)}
<DialogActions>
<Button variant='outlined' color='primary' onClick={() => setOpen(false)}>
OK
</Button>
</DialogActions>
</Dialog>
</>
)
}
function getTorrent(hash, callback) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'get', hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(
json => {
callback(json, null)
},
error => {
callback(null, error)
},
)
} catch (e) {
console.error(e)
}
}
function deleteTorrent(torrent) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'rem',
hash: torrent.hash,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
} catch (e) {
console.error(e)
}
}
function dropTorrent(torrent) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'drop',
hash: torrent.hash,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
} catch (e) {
console.error(e)
}
}

View File

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

View File

@@ -0,0 +1,118 @@
import 'fontsource-roboto'
import { forwardRef, useState } from 'react'
import { UnfoldMore as UnfoldMoreIcon, Close as CloseIcon, Delete as DeleteIcon } from '@material-ui/icons'
import { getPeerString, humanizeSize, shortenText } from 'utils/Utils'
import { torrentsHost } from 'utils/Hosts'
import { NoImageIcon } from 'icons'
import DialogTorrentDetailsContent from 'components/DialogTorrentDetailsContent'
import Dialog from '@material-ui/core/Dialog'
import Slide from '@material-ui/core/Slide'
import { Button, DialogActions, DialogTitle, useMediaQuery, useTheme } from '@material-ui/core'
import axios from 'axios'
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style'
const Transition = forwardRef((props, ref) => <Slide direction='up' ref={ref} {...props} />)
export default function Torrent({ torrent }) {
const [isDetailedInfoOpened, setIsDetailedInfoOpened] = useState(false)
const [isDeleteTorrentOpened, setIsDeleteTorrentOpened] = useState(false)
const theme = useTheme()
const fullScreen = useMediaQuery(theme.breakpoints.down('md'))
const openDetailedInfo = () => setIsDetailedInfoOpened(true)
const closeDetailedInfo = () => setIsDetailedInfoOpened(false)
const openDeleteTorrentAlert = () => setIsDeleteTorrentOpened(true)
const closeDeleteTorrentAlert = () => setIsDeleteTorrentOpened(false)
const { title, name, poster, torrent_size: torrentSize, download_speed: downloadSpeed, hash } = torrent
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
const deleteTorrent = () => axios.post(torrentsHost(), { action: 'rem', hash })
return (
<>
<TorrentCard>
<TorrentCardPoster isPoster={poster}>
{poster ? <img src={poster} alt='poster' /> : <NoImageIcon />}
</TorrentCardPoster>
<TorrentCardButtons>
<StyledButton onClick={openDetailedInfo}>
<UnfoldMoreIcon />
<span>Details</span>
</StyledButton>
<StyledButton onClick={() => dropTorrent(torrent)}>
<CloseIcon />
<span>Drop</span>
</StyledButton>
<StyledButton onClick={openDeleteTorrentAlert}>
<DeleteIcon />
<span>Delete</span>
</StyledButton>
</TorrentCardButtons>
<TorrentCardDescription>
<div className='description-title-wrapper'>
<div className='description-section-name'>Name</div>
<div className='description-torrent-title'>{shortenText(title || name, 100)}</div>
</div>
<div className='description-statistics-wrapper'>
<div className='description-statistics-element-wrapper'>
<div className='description-section-name'>Size</div>
<div className='description-statistics-element-value'>{torrentSize > 0 && humanizeSize(torrentSize)}</div>
</div>
<div className='description-statistics-element-wrapper'>
<div className='description-section-name'>Speed</div>
<div className='description-statistics-element-value'>
{downloadSpeed > 0 ? humanizeSize(downloadSpeed) : '---'}
</div>
</div>
<div className='description-statistics-element-wrapper'>
<div className='description-section-name'>Peers</div>
<div className='description-statistics-element-value'>{getPeerString(torrent) || '---'}</div>
</div>
</div>
</TorrentCardDescription>
</TorrentCard>
<Dialog
open={isDetailedInfoOpened}
onClose={closeDetailedInfo}
fullScreen={fullScreen}
fullWidth
maxWidth='xl'
TransitionComponent={Transition}
>
<DialogTorrentDetailsContent closeDialog={closeDetailedInfo} torrent={torrent} />
</Dialog>
<Dialog open={isDeleteTorrentOpened} onClose={closeDeleteTorrentAlert}>
<DialogTitle>Delete Torrent?</DialogTitle>
<DialogActions>
<Button variant='outlined' onClick={closeDeleteTorrentAlert} color='primary'>
Cancel
</Button>
<Button
variant='contained'
onClick={() => {
deleteTorrent(torrent)
closeDeleteTorrentAlert()
}}
color='primary'
autoFocus
>
Ok
</Button>
</DialogActions>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,206 @@
import styled, { css } from 'styled-components'
export const TorrentCard = styled.div`
border-radius: 5px;
display: grid;
grid-template-columns: 120px 260px 1fr;
grid-template-rows: 180px;
grid-template-areas: 'poster description buttons';
gap: 10px;
padding: 10px;
background: #3fb57a;
box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
@media (max-width: 1260px), (max-height: 500px) {
grid-template-areas:
'poster description'
'buttons buttons';
grid-template-columns: 70px 1fr;
grid-template-rows: 110px max-content;
}
@media (max-width: 770px) {
grid-template-columns: 60px 1fr;
grid-template-rows: 90px max-content;
}
`
export const TorrentCardPoster = styled.div`
grid-area: poster;
border-radius: 5px;
overflow: hidden;
text-align: center;
${({ isPoster }) =>
isPoster
? css`
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 5px;
}
`
: css`
display: grid;
place-items: center;
background: #74c39c;
border: 1px solid #337a57;
svg {
transform: translateY(-3px);
}
`};
@media (max-width: 1260px), (max-height: 500px) {
svg {
width: 50%;
}
}
`
export const TorrentCardButtons = styled.div`
grid-area: buttons;
display: grid;
gap: 10px;
@media (max-width: 1260px), (max-height: 500px) {
grid-template-columns: repeat(3, 1fr);
}
`
export const TorrentCardDescription = styled.div`
grid-area: description;
background: #74c39c;
border-radius: 5px;
padding: 5px;
display: grid;
grid-template-rows: 55% 1fr;
gap: 10px;
@media (max-width: 770px) {
grid-template-rows: 60% 1fr;
gap: 3px;
}
@media (max-width: 770px) {
grid-template-rows: 56% 1fr;
}
.description-title-wrapper {
display: flex;
flex-direction: column;
}
.description-section-name {
text-transform: uppercase;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.4px;
color: #216e47;
@media (max-width: 770px) {
font-size: 0.4rem;
}
}
.description-torrent-title {
overflow: auto;
word-break: break-all;
}
.description-statistics-wrapper {
display: grid;
grid-template-columns: 80px 80px 1fr;
align-self: end;
@media (max-width: 1260px), (max-height: 500px) {
grid-template-columns: 70px 70px 1fr;
}
@media (max-width: 770px) {
grid-template-columns: 65px 65px 1fr;
}
@media (max-width: 700px) {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}
.description-statistics-element-wrapper {
}
.description-statistics-element-value {
margin-left: 5px;
margin-bottom: 10px;
word-break: break-all;
@media (max-width: 1260px), (max-height: 500px) {
font-size: 0.7rem;
margin-bottom: 0;
margin-left: 0;
}
}
.description-torrent-title,
.description-statistics-element-value {
@media (max-width: 770px) {
font-size: 0.6rem;
}
@media (max-width: 410px) {
font-size: 10px;
}
}
`
export const StyledButton = styled.button`
border-radius: 5px;
border: none;
cursor: pointer;
transition: 0.2s;
display: flex;
align-items: center;
text-transform: uppercase;
background: #268757;
color: #fff;
font-size: 1rem;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
letter-spacing: 0.009em;
padding: 10px 20px;
:hover {
background: #2a7e54;
}
> :first-child {
margin-right: 10px;
}
@media (max-width: 1260px), (max-height: 500px) {
padding: 5px 10px;
font-size: 0.8rem;
svg {
width: 20px;
}
}
@media (max-width: 770px) {
font-size: 0.7rem;
svg {
width: 15px;
}
}
@media (max-width: 420px) {
padding: 7px 10px;
justify-content: center;
svg {
display: none;
}
}
`

View File

@@ -1,72 +1,57 @@
import styled from 'styled-components'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { Typography } from '@material-ui/core' import { Typography } from '@material-ui/core'
import { torrentsHost } from 'utils/Hosts' import { torrentsHost } from 'utils/Hosts'
import Torrent from 'components/Torrent' import TorrentCard from 'components/TorrentCard'
import axios from 'axios'
const TorrentListWrapper = styled.div` import CircularProgress from '@material-ui/core/CircularProgress'
display: grid; import { TorrentListWrapper, CenteredGrid } from 'App/style'
grid-template-columns: repeat(auto-fit, 350px);
gap: 30px;
@media (max-width: 600px), (max-height: 500px) {
gap: 10px;
grid-template-columns: repeat(auto-fit, 310px);
}
@media (max-width: 410px) {
grid-template-columns: minmax(min-content, 290px);
}
`
const getTorrentList = (callback, errorCallback) => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'list' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(callback)
.catch(() => errorCallback())
}
export default function TorrentList() { export default function TorrentList() {
const [torrents, setTorrents] = useState([]) const [torrents, setTorrents] = useState([])
const [offline, setOffline] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isOffline, setIsOffline] = useState(true)
const timerID = useRef(-1) const timerID = useRef(-1)
const updateTorrentList = torrs => {
setTorrents(torrs)
setOffline(false)
}
const resetTorrentList = () => {
setTorrents([])
setOffline(true)
}
useEffect(() => { useEffect(() => {
timerID.current = setInterval(() => { timerID.current = setInterval(() => {
getTorrentList(updateTorrentList, resetTorrentList) // getting torrent list
axios
.post(torrentsHost(), { action: 'list' })
.then(({ data }) => {
// updating torrent list
setTorrents(data)
setIsOffline(false)
})
.catch(() => {
// resetting torrent list
setTorrents([])
setIsOffline(true)
})
.finally(() => setIsLoading(false))
}, 1000) }, 1000)
return () => { return () => clearInterval(timerID.current)
clearInterval(timerID.current)
}
}, []) }, [])
if (isLoading || isOffline || !torrents.length) {
return (
<CenteredGrid>
{isLoading ? (
<CircularProgress />
) : isOffline ? (
<Typography>Offline</Typography>
) : (
!torrents.length && <Typography>No torrents added</Typography>
)}
</CenteredGrid>
)
}
return ( return (
<TorrentListWrapper> <TorrentListWrapper>
{offline ? ( {torrents.map(torrent => (
<Typography>Offline</Typography> <TorrentCard key={torrent.hash} torrent={torrent} />
) : !torrents.length ? ( ))}
<Typography>No torrents added</Typography>
) : (
torrents && torrents.map(torrent => <Torrent key={torrent.hash} torrent={torrent} />)
)}
</TorrentListWrapper> </TorrentListWrapper>
) )
} }

View File

@@ -3,40 +3,22 @@ import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem' import ListItem from '@material-ui/core/ListItem'
import PublishIcon from '@material-ui/icons/Publish' import PublishIcon from '@material-ui/icons/Publish'
import { torrentUploadHost } from 'utils/Hosts' import { torrentUploadHost } from 'utils/Hosts'
import axios from 'axios'
const classes = {
input: {
display: 'none',
},
}
export default function UploadDialog() { export default function UploadDialog() {
const handleCapture = ({ target }) => { const handleCapture = ({ target: { files } }) => {
const [file] = files
const data = new FormData() const data = new FormData()
data.append('save', 'true') data.append('file', file)
for (let i = 0; i < target.files.length; i++) { axios.post(torrentUploadHost(), data)
data.append(`file${i}`, target.files[i])
}
fetch(torrentUploadHost(), {
method: 'POST',
body: data,
})
} }
return ( return (
<div> <div>
<label htmlFor='raised-button-file'> <label htmlFor='raised-button-file'>
<input <input onChange={handleCapture} accept='*/*' type='file' style={{ display: 'none' }} id='raised-button-file' />
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'> <ListItem button variant='raised' type='submit' component='span' key='Upload file'>
<ListItemIcon> <ListItemIcon>
<PublishIcon /> <PublishIcon />
</ListItemIcon> </ListItemIcon>

1
web/src/torrentStates.js Normal file
View File

@@ -0,0 +1 @@
export const [GETTING_INFO, PRELOAD, WORKING, CLOSED, IN_DB] = [1, 2, 3, 4, 5]

View File

@@ -8,3 +8,6 @@ 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}`
} }
export const shortenText = (text, sympolAmount) =>
text.slice(0, sympolAmount) + (text.length > sympolAmount ? '...' : '')

View File

@@ -1174,7 +1174,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.14.0" version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
@@ -2748,6 +2748,13 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34"
integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA== integrity sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
axobject-query@^2.2.0: axobject-query@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -4047,6 +4054,13 @@ copy-props@^2.0.1:
each-props "^1.3.2" each-props "^1.3.2"
is-plain-object "^5.0.0" is-plain-object "^5.0.0"
copy-to-clipboard@^3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
dependencies:
toggle-selection "^1.0.6"
core-js-compat@^3.6.2, core-js-compat@^3.9.0, core-js-compat@^3.9.1: core-js-compat@^3.6.2, core-js-compat@^3.9.0, core-js-compat@^3.9.1:
version "3.13.0" version "3.13.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.13.0.tgz#a88f5fa81d8e9b15d7f98abc4447a4dfca2a358f" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.13.0.tgz#a88f5fa81d8e9b15d7f98abc4447a4dfca2a358f"
@@ -5727,7 +5741,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@^1.0.0: follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
@@ -5927,6 +5941,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3" has "^1.0.3"
has-symbols "^1.0.1" has-symbols "^1.0.1"
get-node-dimensions@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823"
integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==
get-own-enumerable-property-symbols@^3.0.0: get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -7826,6 +7845,11 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
konva@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/konva/-/konva-8.0.1.tgz#f34f483cdf62c36f966addc1a7484ed694313c2b"
integrity sha512-QDppGS1L5Dhod1zjwy9GVVjeyfPBHnPncL5oRh1NyjR1mEvhrLjzflrkdW+p73uFIW9hwCDZVLGxzzjQre9izw==
language-subtag-registry@~0.3.2: language-subtag-registry@~0.3.2:
version "0.3.21" version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@@ -8041,7 +8065,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@^4.7.0: "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5, lodash@^4.7.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -9033,6 +9057,11 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
parse-torrent-title@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/parse-torrent-title/-/parse-torrent-title-1.3.0.tgz#3dedea10277b17998b124a4fd67d9e190b0306b8"
integrity sha512-R5wya73/Ef0qUhb9177Ko8nRQyN1ziWD5DPnlrDrrgcchUnmIrG//cPENunvFYRZCLDZosXTKTo7TpQ2Pgbryg==
parse5@6.0.1: parse5@6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
@@ -10215,6 +10244,14 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7" regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1" whatwg-fetch "^3.4.1"
react-copy-to-clipboard@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4"
integrity sha512-9S3j+m+UxDZOM0Qb8mhnT/rMR0NGSrj9A/073yz2DSxPMYhmYFBMYIdI2X4o8AjOjyFsSNxDRnCX6s/gRxpriw==
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
react-dev-utils@^11.0.3: react-dev-utils@^11.0.3:
version "11.0.4" version "11.0.4"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a"
@@ -10245,6 +10282,11 @@ react-dev-utils@^11.0.3:
strip-ansi "6.0.0" strip-ansi "6.0.0"
text-table "0.2.0" text-table "0.2.0"
react-div-100vh@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-div-100vh/-/react-div-100vh-0.6.0.tgz#577972d8ac17693edcd44061c1a4b5a7578e49ec"
integrity sha512-ErV0VTNXUd8jZqofC0ExZr5u+XDD2kN2te4SbwtqsyTm0UOjVYu53kP+FalGQrTe+DoMG8VYR2dITcAFu7c/5w==
react-dom@^17.0.2: react-dom@^17.0.2:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -10269,6 +10311,33 @@ react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-konva@^17.0.2-4:
version "17.0.2-4"
resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-4.tgz#afd0968e1295b624bf2a7a154ba294e0d5be55cd"
integrity sha512-YvRVPT81y8sMQV1SY1/tIDetGxBK+7Rk86u4LmiyDBLLE17vD78F01b8EC3AuP3nI3hUaTblfBugUF35cm6Etg==
dependencies:
react-reconciler "~0.26.2"
scheduler "^0.20.2"
react-measure@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.5.2.tgz#4ffc410e8b9cb836d9455a9ff18fc1f0fca67f89"
integrity sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==
dependencies:
"@babel/runtime" "^7.2.0"
get-node-dimensions "^1.2.1"
prop-types "^15.6.2"
resize-observer-polyfill "^1.5.0"
react-reconciler@~0.26.2:
version "0.26.2"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91"
integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
react-refresh@^0.8.3: react-refresh@^0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@@ -10638,6 +10707,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -11966,6 +12040,11 @@ to-through@^2.0.0:
dependencies: dependencies:
through2 "^2.0.3" through2 "^2.0.3"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
toidentifier@1.0.0: toidentifier@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"