diff --git a/web/.eslintrc b/web/.eslintrc index 929c132..02f9504 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -36,6 +36,7 @@ "react/prop-types": 0, "react/react-in-jsx-scope": 0, "react/jsx-uses-react": 0, - "import/no-unresolved": 0 // used to allow relative paths from "src" folder + "import/no-unresolved": 0, // used to allow relative paths from "src" folder + "react/jsx-props-no-spreading": 0 } } \ No newline at end of file diff --git a/web/package.json b/web/package.json index 261134a..7984c45 100644 --- a/web/package.json +++ b/web/package.json @@ -5,11 +5,19 @@ "dependencies": { "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", + "axios": "^0.21.1", "clsx": "^1.1.1", "fontsource-roboto": "^4.0.0", + "konva": "^8.0.1", + "lodash": "^4.17.21", "material-ui-image": "^3.3.2", + "parse-torrent-title": "^1.3.0", "react": "^17.0.2", + "react-copy-to-clipboard": "^5.0.3", + "react-div-100vh": "^0.6.0", "react-dom": "^17.0.2", + "react-konva": "^17.0.2-4", + "react-measure": "^2.5.2", "react-scripts": "4.0.3", "styled-components": "^5.3.0" }, diff --git a/web/public/index.html b/web/public/index.html index 569cd1a..1fe47d0 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -10,44 +10,6 @@ TorrServer -
diff --git a/web/src/App.jsx b/web/src/App.jsx deleted file mode 100644 index 7286c36..0000000 --- a/web/src/App.jsx +++ /dev/null @@ -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 ( - - - - - ) -} diff --git a/web/src/App/Sidebar.jsx b/web/src/App/Sidebar.jsx new file mode 100644 index 0000000..5d6edac --- /dev/null +++ b/web/src/App/Sidebar.jsx @@ -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 ( + + + + + + + + + + + + + + + + + + + fetch(shutdownHost())}> + + + + + + + + + + + setIsDonationDialogOpen(true)}> + + + + + + + + ) +} diff --git a/web/src/App/index.jsx b/web/src/App/index.jsx new file mode 100644 index 0000000..ce7164a --- /dev/null +++ b/web/src/App/index.jsx @@ -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 ( + + + + {/* Div100vh - iOS WebKit fix */} + + + + setIsDrawerOpen(!isDrawerOpen)} + edge='start' + > + {isDrawerOpen ? : } + + + + TorrServer {torrServerVersion} + + + + + + + + {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} + {!JSON.parse(localStorage.getItem('snackbarIsClosed')) && } + + + + ) +} diff --git a/web/src/App/style.js b/web/src/App/style.js new file mode 100644 index 0000000..bebad80 --- /dev/null +++ b/web/src/App/style.js @@ -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; + } +` diff --git a/web/src/components/About.jsx b/web/src/components/About.jsx index bbcd699..a241eee 100644 --- a/web/src/components/About.jsx +++ b/web/src/components/About.jsx @@ -3,7 +3,6 @@ import Button from '@material-ui/core/Button' import Dialog from '@material-ui/core/Dialog' import DialogActions from '@material-ui/core/DialogActions' import DialogContent from '@material-ui/core/DialogContent' -import DialogContentText from '@material-ui/core/DialogContentText' import DialogTitle from '@material-ui/core/DialogTitle' import InfoIcon from '@material-ui/icons/Info' import ListItem from '@material-ui/core/ListItem' @@ -27,23 +26,21 @@ export default function AboutDialog() { - -
-

Thanks to everyone who tested and helped.

-
-
-

Special thanks:

- Anacrolix Matt Joiner github.com/anacrolix -
- tsynik nikk Никита github.com/tsynik -
- dancheskus github.com/dancheskus -
- Tw1cker Руслан Пахнев github.com/Nemiroff -
- SpAwN_LMG -
-
+
+

Thanks to everyone who tested and helped.

+
+
+

Special thanks:

+ Anacrolix Matt Joiner github.com/anacrolix +
+ tsynik nikk Никита github.com/tsynik +
+ dancheskus github.com/dancheskus +
+ Tw1cker Руслан Пахнев github.com/Nemiroff +
+ SpAwN_LMG +
diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index dbbbe72..4c7225f 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -63,7 +63,7 @@ export default function AddDialog({ handleClose }) { Cancel - diff --git a/web/src/components/Appbar/index.jsx b/web/src/components/Appbar/index.jsx deleted file mode 100644 index a4f1eec..0000000 --- a/web/src/components/Appbar/index.jsx +++ /dev/null @@ -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('')) setTSVersion(txt) - }) - }, [isDrawerOpen]) - - return ( -
- - - - - - - TorrServer {tsVersion} - - - - - -
- - {theme.direction === 'rtl' ? : } - -
- - - - - - - - - - - - - - - - - - - - - fetch(shutdownHost())}> - - - - - - - - - - - setIsDonationDialogOpen(true)}> - - - - - - -
- -
-
- - -
- - {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} - {!JSON.parse(localStorage.getItem('snackbarIsClosed')) && } -
- ) -} diff --git a/web/src/components/Appbar/useStyles.js b/web/src/components/Appbar/useStyles.js deleted file mode 100644 index d46b1d0..0000000 --- a/web/src/components/Appbar/useStyles.js +++ /dev/null @@ -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), - }, -})) diff --git a/web/src/components/DialogCacheInfo.jsx b/web/src/components/DialogCacheInfo.jsx deleted file mode 100644 index ffdef9f..0000000 --- a/web/src/components/DialogCacheInfo.jsx +++ /dev/null @@ -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 ( -
- - - Hash {cache.Hash} -
- Capacity {humanizeSize(cache.Capacity)} -
- Filled {humanizeSize(cache.Filled)} -
- Torrent size {cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)} -
- Pieces length {humanizeSize(cache.PiecesLength)} -
- Pieces count {cache.PiecesCount} -
- Peers: {getPeerString(cache.Torrent)} -
- Download speed {' '} - {cache.Torrent && cache.Torrent.download_speed ? `${humanizeSize(cache.Torrent.download_speed)}/sec` : ''} -
- Upload speed {' '} - {cache.Torrent && cache.Torrent.upload_speed ? `${humanizeSize(cache.Torrent.upload_speed)}/sec` : ''} -
- Status {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string} -
-
- - -
- {pMap.map(itm => ( - - {itm.prc > 0 && itm.prc < 100 && ( -
- )} - - ))} -
- -
- ) -} - -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 - } - } -} - */ diff --git a/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx new file mode 100644 index 0000000..a65a7e5 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx @@ -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 ( + <> + + Data + + + + + + + + + + + + + Cache + + + + ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/DetailedView/style.js b/web/src/components/DialogTorrentDetailsContent/DetailedView/style.js new file mode 100644 index 0000000..3fd9431 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/DetailedView/style.js @@ -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; + } +` diff --git a/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx new file mode 100644 index 0000000..520ed60 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/DialogHeader.jsx @@ -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 ( + + + + {onBack ? : } + + + + {title} + + + {onBack && ( + + )} + + + ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx b/web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx new file mode 100644 index 0000000..96268f6 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx @@ -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 ( + + ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/StatisticsField.jsx b/web/src/components/DialogTorrentDetailsContent/StatisticsField.jsx new file mode 100644 index 0000000..c6b0e65 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/StatisticsField.jsx @@ -0,0 +1,14 @@ +import { WidgetFieldWrapper, WidgetFieldIcon, WidgetFieldValue, WidgetFieldTitle } from './style' + +export default function StatisticsField({ icon: Icon, title, value, iconBg, valueBg }) { + return ( + + {title} + + + + + {value} + + ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/Table/index.jsx b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx new file mode 100644 index 0000000..87952d0 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/Table/index.jsx @@ -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' + ) : ( + <> + + + + viewed + name + {fileHasSeasonText && seasonAmount?.length === 1 && season} + {fileHasEpisodeText && episode} + {fileHasResolutionText && resolution} + size + actions + + + + + {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) && ( + + + {title} + {fileHasSeasonText && seasonAmount?.length === 1 && {season}} + {fileHasEpisodeText && {episode}} + {fileHasResolutionText && {resolution}} + {humanizeSize(length)} + + + + + + + + + + + + + ) + ) + })} + + + + + {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) && ( + +
{title}
+
+ {isViewed && ( +
+
viewed
+
+
+
+
+ )} + {fileHasSeasonText && seasonAmount?.length === 1 && ( +
+
season
+
{season}
+
+ )} + {fileHasEpisodeText && ( +
+
epoisode
+
{episode}
+
+ )} + {fileHasResolutionText && ( +
+
resolution
+
{resolution}
+
+ )} +
+
size
+
{humanizeSize(length)}
+
+
+
+ + + + + + + + + +
+ + ) + ) + })} + + + ) + }, + (prev, next) => isEqual(prev, next), +) + +export default Table diff --git a/web/src/components/DialogTorrentDetailsContent/Table/style.js b/web/src/components/DialogTorrentDetailsContent/Table/style.js new file mode 100644 index 0000000..98440fd --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/Table/style.js @@ -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; + } + } + } + `} +` diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx new file mode 100644 index 0000000..de600e7 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx @@ -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 ( + setDimensions(bounds)}> + {({ measureRef }) => ( +
+ + + + {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 && ( + + ) + ) : ( + + ) + })} + + + +
+ )} +
+ ) + }, + (prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers), +) + +export default TorrentCache diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx new file mode 100644 index 0000000..e460a87 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/index.jsx @@ -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 && ( + <> + Download Playlist + + Latest file played: {latestViewedFileData?.title}. + {latestViewedFileData?.season && ( + <> + {' '} + Season: {latestViewedFileData?.season}. Episode: {latestViewedFileData?.episode}. + + )} + + + + + + + + + + + + + )} + Torrent State + + + + + Info + + {(isOnlyOnePlayableFile || !viewedFileList?.length) && ( + + + + )} + + + + + + ) + }, + () => true, +) + +export default TorrentFunctions diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/style.js b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/style.js new file mode 100644 index 0000000..0955c5e --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentFunctions/style.js @@ -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`}; + } + `} +` diff --git a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx new file mode 100644 index 0000000..5eeed67 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx @@ -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 +} diff --git a/web/src/components/DialogTorrentDetailsContent/helpers.js b/web/src/components/DialogTorrentDetailsContent/helpers.js new file mode 100644 index 0000000..cb164e5 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/helpers.js @@ -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)) diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx new file mode 100644 index 0000000..4f6eb26 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -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 = () => ( +
+ +
+) + +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 ( + <> + setIsDetailedCacheView(false) })} + /> + +
+ {isLoading ? ( + + ) : isDetailedCacheView ? ( + + ) : ( + + + {poster ? poster : } + +
+ {name && name !== title ? ( + <> + {shortenText(getTitle(name), 50)} + {shortenText(title, 160)} + + ) : ( + {shortenText(getTitle(title), 50)} + )} + + + + + + + + + + + +
+
+ + + + Buffer + {!settings?.PreloadBuffer && ( + Enable "Preload Buffer" in settings to change buffer size + )} + + + + + + + + + Torrent Content + + {seasonAmount?.length > 1 && ( + <> + Select Season + + {seasonAmount.map(season => ( + + ))} + + + Season {selectedSeason} + + )} + + + + + )} + + + ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/style.js b/web/src/components/DialogTorrentDetailsContent/style.js new file mode 100644 index 0000000..69beb40 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/style.js @@ -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; +` diff --git a/web/src/components/DialogTorrentDetailsContent/widgets.jsx b/web/src/components/DialogTorrentDetailsContent/widgets.jsx new file mode 100644 index 0000000..6a59f0c --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/widgets.jsx @@ -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 }) => ( + +) + +export const UploadSpeedWidget = ({ data }) => ( + +) + +export const PeersWidget = ({ data }) => ( + +) + +export const PiecesCountWidget = ({ data }) => ( + +) +export const PiecesLengthWidget = ({ data }) => ( + +) +export const StatusWidget = ({ data }) => ( + +) + +export const SizeWidget = ({ data }) => ( + +) diff --git a/web/src/components/DialogTorrentInfo.jsx b/web/src/components/DialogTorrentInfo.jsx deleted file mode 100644 index 61fe20d..0000000 --- a/web/src/components/DialogTorrentInfo.jsx +++ /dev/null @@ -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 ( -
- - - - {torrentLocalComponentValue.poster && ( - - )} - - - {torrentLocalComponentValue.title}{' '} - {torrentLocalComponentValue.name && - torrentLocalComponentValue.name !== torrentLocalComponentValue.title && - ` | ${torrentLocalComponentValue.name}`} - - Peers: {getPeerString(torrentLocalComponentValue)} -
- Loaded: {getPreload(torrentLocalComponentValue)} -
- Speed: {humanizeSize(torrentLocalComponentValue.download_speed)} -
- Status: {torrentLocalComponentValue.stat_string} -
-
-
-
- {torrentLocalComponentValue.stat === 2 && ( - - )} -
- - - - - - - - - - - - {getPlayableFile(torrentLocalComponentValue) && - getPlayableFile(torrentLocalComponentValue).map(file => ( - - - - - ))} - - -
- ) -} - -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', -] diff --git a/web/src/components/RemoveAll.jsx b/web/src/components/RemoveAll.jsx index fd81770..aac5c0f 100644 --- a/web/src/components/RemoveAll.jsx +++ b/web/src/components/RemoveAll.jsx @@ -1,7 +1,9 @@ +import { Button, Dialog, DialogActions, DialogTitle } from '@material-ui/core' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' import DeleteIcon from '@material-ui/icons/Delete' +import { useState } from 'react' import { torrentsHost } from 'utils/Hosts' const fnRemoveAll = () => { @@ -29,13 +31,40 @@ const fnRemoveAll = () => { } export default function RemoveAll() { - return ( - - - - + const [open, setOpen] = useState(false) + const closeDialog = () => setOpen(false) + const openDialog = () => setOpen(true) - - + return ( + <> + + + + + + + + + + Delete Torrent? + + + + + + + ) } diff --git a/web/src/components/Settings.jsx b/web/src/components/Settings.jsx index ceb623a..1130683 100644 --- a/web/src/components/Settings.jsx +++ b/web/src/components/Settings.jsx @@ -26,7 +26,6 @@ export default function SettingsDialog() { setOpen(false) const sets = JSON.parse(JSON.stringify(settings)) sets.CacheSize *= 1024 * 1024 - sets.PreloadBufferSize *= 1024 * 1024 fetch(settingsHost(), { method: 'post', body: JSON.stringify({ action: 'set', sets }), @@ -51,8 +50,6 @@ export default function SettingsDialog() { json => { // eslint-disable-next-line no-param-reassign json.CacheSize /= 1024 * 1024 - // eslint-disable-next-line no-param-reassign - json.PreloadBufferSize /= 1024 * 1024 setSets(json) setShow(true) }, @@ -135,13 +132,7 @@ export default function SettingsDialog() {

Retracker mode - diff --git a/web/src/components/Torrent/index.jsx b/web/src/components/Torrent/index.jsx deleted file mode 100644 index f236d0f..0000000 --- a/web/src/components/Torrent/index.jsx +++ /dev/null @@ -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 ( - <> - - - {poster ? poster : } - - - - { - setShowCache(true) - setOpen(true) - }} - > - - Cache - - - dropTorrent(torrentLocalComponentValue)}> - - Drop - - - { - setShowCache(false) - setOpen(true) - }} - > - - Details - - - deleteTorrent(torrentLocalComponentValue)}> - - Delete - - - - - - Name - {title || name} - - - - - Size - - {torrent_size > 0 && humanizeSize(torrent_size)} - - - - - Speed - - {download_speed > 0 ? humanizeSize(download_speed) : '---'} - - - - - Peers - - {getPeerString(torrentLocalComponentValue) || '---'} - - - - - - - setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'> - {!showCache ? ( - - ) : ( - - )} - - - - - - ) -} - -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) - } -} diff --git a/web/src/components/Torrent/style.js b/web/src/components/Torrent/style.js deleted file mode 100644 index 221a6a0..0000000 --- a/web/src/components/Torrent/style.js +++ /dev/null @@ -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; - } -` diff --git a/web/src/components/TorrentCard/index.jsx b/web/src/components/TorrentCard/index.jsx new file mode 100644 index 0000000..022216c --- /dev/null +++ b/web/src/components/TorrentCard/index.jsx @@ -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) => ) + +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 ( + <> + + + {poster ? poster : } + + + + + + Details + + + dropTorrent(torrent)}> + + Drop + + + + + Delete + + + + +
+
Name
+
{shortenText(title || name, 100)}
+
+ +
+
+
Size
+
{torrentSize > 0 && humanizeSize(torrentSize)}
+
+ +
+
Speed
+
+ {downloadSpeed > 0 ? humanizeSize(downloadSpeed) : '---'} +
+
+ +
+
Peers
+
{getPeerString(torrent) || '---'}
+
+
+
+
+ + + + + + + Delete Torrent? + + + + + + + + ) +} diff --git a/web/src/components/TorrentCard/style.js b/web/src/components/TorrentCard/style.js new file mode 100644 index 0000000..e9da211 --- /dev/null +++ b/web/src/components/TorrentCard/style.js @@ -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; + } + } +` diff --git a/web/src/components/TorrentList.jsx b/web/src/components/TorrentList.jsx index 4fbaaa7..c75c9d2 100644 --- a/web/src/components/TorrentList.jsx +++ b/web/src/components/TorrentList.jsx @@ -1,72 +1,57 @@ -import styled from 'styled-components' import { useEffect, useRef, useState } from 'react' import { Typography } from '@material-ui/core' import { torrentsHost } from 'utils/Hosts' -import Torrent from 'components/Torrent' - -const TorrentListWrapper = styled.div` - display: grid; - 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()) -} +import TorrentCard from 'components/TorrentCard' +import axios from 'axios' +import CircularProgress from '@material-ui/core/CircularProgress' +import { TorrentListWrapper, CenteredGrid } from 'App/style' export default function TorrentList() { const [torrents, setTorrents] = useState([]) - const [offline, setOffline] = useState(true) + const [isLoading, setIsLoading] = useState(true) + const [isOffline, setIsOffline] = useState(true) const timerID = useRef(-1) - const updateTorrentList = torrs => { - setTorrents(torrs) - setOffline(false) - } - - const resetTorrentList = () => { - setTorrents([]) - setOffline(true) - } - useEffect(() => { 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) - return () => { - clearInterval(timerID.current) - } + return () => clearInterval(timerID.current) }, []) + if (isLoading || isOffline || !torrents.length) { + return ( + + {isLoading ? ( + + ) : isOffline ? ( + Offline + ) : ( + !torrents.length && No torrents added + )} + + ) + } + return ( - {offline ? ( - Offline - ) : !torrents.length ? ( - No torrents added - ) : ( - torrents && torrents.map(torrent => ) - )} + {torrents.map(torrent => ( + + ))} ) } diff --git a/web/src/components/Upload.jsx b/web/src/components/Upload.jsx index e2af75e..4eba3b4 100644 --- a/web/src/components/Upload.jsx +++ b/web/src/components/Upload.jsx @@ -3,40 +3,22 @@ import ListItemText from '@material-ui/core/ListItemText' import ListItem from '@material-ui/core/ListItem' import PublishIcon from '@material-ui/icons/Publish' import { torrentUploadHost } from 'utils/Hosts' - -const classes = { - input: { - display: 'none', - }, -} +import axios from 'axios' export default function UploadDialog() { - const handleCapture = ({ target }) => { + const handleCapture = ({ target: { files } }) => { + const [file] = files const data = new FormData() - data.append('save', 'true') - for (let i = 0; i < target.files.length; i++) { - data.append(`file${i}`, target.files[i]) - } - fetch(torrentUploadHost(), { - method: 'POST', - body: data, - }) + data.append('file', file) + axios.post(torrentUploadHost(), data) } return (