From 6b5a572918ec0a74883411f3118dafb198b26c09 Mon Sep 17 00:00:00 2001 From: Daniel Shleifman Date: Thu, 27 May 2021 17:21:39 +0300 Subject: [PATCH] added full width dialog for cache and info --- web/package.json | 1 + web/src/components/DialogCacheInfo/index.jsx | 86 +----- .../customHooks.jsx | 74 +++++ .../DialogTorrentDetailsContent/index.jsx | 275 ++++++++++++++++++ web/src/components/Torrent/index.jsx | 25 +- web/yarn.lock | 9 +- 6 files changed, 382 insertions(+), 88 deletions(-) create mode 100644 web/src/components/DialogTorrentDetailsContent/customHooks.jsx create mode 100644 web/src/components/DialogTorrentDetailsContent/index.jsx diff --git a/web/package.json b/web/package.json index 5c00cee..99ee976 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "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", diff --git a/web/src/components/DialogCacheInfo/index.jsx b/web/src/components/DialogCacheInfo/index.jsx index 79c6a29..1e6b5b3 100644 --- a/web/src/components/DialogCacheInfo/index.jsx +++ b/web/src/components/DialogCacheInfo/index.jsx @@ -1,19 +1,15 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, 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' import { Stage, Layer } from 'react-konva' import Measure from 'react-measure' +import { useUpdateCache, useCreateCacheMap } from 'components/DialogTorrentDetailsContent/customHooks' import SingleBlock from './SingleBlock' export default function DialogCacheInfo({ hash }) { - const [cache, setCache] = useState({}) - const [pMap, setPMap] = useState([]) - const timerID = useRef(null) - const componentIsMounted = useRef(true) const [dimensions, setDimensions] = useState({ width: -1, height: -1 }) const [isShortView, setIsShortView] = useState(true) const [isLoading, setIsLoading] = useState(true) @@ -24,6 +20,9 @@ export default function DialogCacheInfo({ hash }) { stageOffset: null, }) + const cache = useUpdateCache(hash) + const cacheMap = useCreateCacheMap(cache, () => setIsLoading(false)) + const updateStageSettings = (boxHeight, strokeWidth) => { setStageSettings({ boxHeight, @@ -36,59 +35,8 @@ export default function DialogCacheInfo({ hash }) { useEffect(() => { // initializing stageSettings updateStageSettings(24, 4) - - return () => { - // this function is required to notify "getCache" when NOT to make state update - componentIsMounted.current = false - } }, []) - useEffect(() => { - if (hash) { - timerID.current = setInterval(() => { - getCache(hash, value => { - // this is required to avoid memory leak - if (componentIsMounted.current) setCache(value) - }) - }, 100) - } else clearInterval(timerID.current) - - return () => { - clearInterval(timerID.current) - } - }, [hash]) - - 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) - } - - setPMap(map) - setIsLoading(false) - }, [cache]) - const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) @@ -98,7 +46,7 @@ export default function DialogCacheInfo({ hash }) { preloadPiecesAmount === piecesInOneRow ? preloadPiecesAmount - 1 : preloadPiecesAmount + piecesInOneRow - (preloadPiecesAmount % piecesInOneRow) - 1 - const amountOfRows = Math.ceil((isShortView ? amountOfBlocksToRenderInShortView : pMap.length) / piecesInOneRow) + const amountOfRows = Math.ceil((isShortView ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow) let activeId = null return ( @@ -158,7 +106,7 @@ export default function DialogCacheInfo({ hash }) { height={stageOffset + blockSizeWithMargin * amountOfRows} > - {pMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => { + {cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => { const currentRow = Math.floor((isShortView ? id - activeId : id) / piecesInOneRow) // -------- related only for short view ------- @@ -207,26 +155,6 @@ export default function DialogCacheInfo({ hash }) { ) } -const 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(callback, error => { - callback({}) - console.error(error) - }) - } catch (e) { - console.error(e) - callback({}) - } -} /* { "Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e", diff --git a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx new file mode 100644 index 0000000..550ac0a --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react' +import { cacheHost } 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, callback) => { + 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) + callback && callback() + }, [cache, callback]) + + return cacheMap +} diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx new file mode 100644 index 0000000..3de1eb1 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -0,0 +1,275 @@ +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 styled, { css } from 'styled-components' +import { NoImageIcon } from 'icons' +import { getPeerString, humanizeSize } from 'utils/Utils' +import { viewedHost } from 'utils/Hosts' + +import { useUpdateCache, useCreateCacheMap } from './customHooks' + +const useStyles = makeStyles(theme => ({ + appBar: { position: 'relative' }, + title: { marginLeft: theme.spacing(2), flex: 1 }, +})) + +const DialogContent = styled.div` + display: grid; + grid-template-rows: min-content 200px 80px 70px; +` +const Poster = styled.div` + ${({ poster }) => css` + height: 400px; + border-radius: 5px; + overflow: hidden; + + ${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); + } + `} + `} +` +const HeaderSection = styled.section` + padding: 40px; + display: grid; + grid-template-columns: min-content 1fr; + gap: 30px; +` + +const TorrentData = styled.div`` + +const CacheSection = styled.section` + padding: 40px; + background: lightgray; +` + +const ButtonSection = styled.section` + box-shadow: 0px 4px 4px -1px rgb(0 0 0 / 30%); + display: flex; + justify-content: space-evenly; + align-items: center; + text-transform: uppercase; +` + +const ButtonSectionButton = styled.div` + background: lightblue; + height: 100%; + flex: 1; + display: grid; + place-items: center; + cursor: pointer; + font-size: 15px; + + :not(:last-child) { + border-right: 1px solid blue; + } + + :hover { + background: red; + } +` + +const TorrentFilesSection = styled.div`` + +export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { + const classes = useStyles() + const { + poster, + hash, + title, + name, + download_speed: downloadSpeed, + upload_speed: uploadSpeed, + stat_string: statString, + torrent_size: torrentSize, + } = torrent + + const cache = useUpdateCache(hash) + const cacheMap = useCreateCacheMap(cache) + + const { Capacity, PiecesCount, PiecesLength } = cache + + return ( + <> + + + + + + + Torrent Details + + + + + + + + {poster ? poster : } + + +
hash: {hash}
+
title: {title}
+
name: {name}
+
peers: {getPeerString(torrent)}
+
loaded: {getPreload(torrent)}
+
download speed: {humanizeSize(downloadSpeed)}
+
upload speed: {humanizeSize(uploadSpeed)}
+
status: {statString}
+
torrent size: {humanizeSize(torrentSize)}
+ +
Capacity: {humanizeSize(Capacity)}
+
PiecesCount: {PiecesCount}
+
PiecesLength: {humanizeSize(PiecesLength)}
+
+
+ + + + + copy hash + + remove views + + drop torrent + + download playlist + + download playlist after last view + + + +
+ + ) +} + +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) +} + +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() +} +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/Torrent/index.jsx b/web/src/components/Torrent/index.jsx index cca4dd1..bf19069 100644 --- a/web/src/components/Torrent/index.jsx +++ b/web/src/components/Torrent/index.jsx @@ -1,18 +1,20 @@ /* eslint-disable camelcase */ import 'fontsource-roboto' -import { useEffect, useRef, useState } from 'react' -import Button from '@material-ui/core/Button' +import { forwardRef, useEffect, useRef, useState } from 'react' +import DialogActions from '@material-ui/core/DialogActions' +import DialogTorrentInfo from 'components/DialogTorrentInfo' +import DialogCacheInfo from 'components/DialogCacheInfo' 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 DialogTorrentDetailsContent from 'components/DialogTorrentDetailsContent' +import Dialog from '@material-ui/core/Dialog' +import Slide from '@material-ui/core/Slide' +import { Button } from '@material-ui/core' import { StyledButton, @@ -25,6 +27,9 @@ import { TorrentCardDetails, } from './style' +// eslint-disable-next-line react/jsx-props-no-spreading +const Transition = forwardRef((props, ref) => ) + export default function Torrent({ torrent }) { const [open, setOpen] = useState(false) const [showCache, setShowCache] = useState(false) @@ -122,7 +127,11 @@ export default function Torrent({ torrent }) { - setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'> + + setOpen(false)} torrent={torrentLocalComponentValue} /> + + + {/* {showCache ? ( ) : ( @@ -133,7 +142,7 @@ export default function Torrent({ torrent }) { OK - + */} ) } diff --git a/web/yarn.lock b/web/yarn.lock index 162396c..ec2581b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2748,6 +2748,13 @@ axe-core@^4.0.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.2.1.tgz#2e50bcf10ee5b819014f6e342e41e45096239e34" 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: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5727,7 +5734,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.14.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==