diff --git a/web/package.json b/web/package.json index 70e37dd..ba42718 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,8 @@ "react-konva": "^17.0.2-4", "react-measure": "^2.5.2", "react-scripts": "4.0.3", + "react-virtualized-auto-sizer": "^1.0.5", + "react-window": "^1.8.6", "styled-components": "^5.3.0", "uuid": "^8.3.2" }, diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx deleted file mode 100644 index db29707..0000000 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache.jsx +++ /dev/null @@ -1,138 +0,0 @@ -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/isEqual' -import styled from 'styled-components' -import { v4 as uuidv4 } from 'uuid' - -import SingleBlock from './SingleBlock' -import { useCreateCacheMap } from './customHooks' - -const ScrollNotification = styled.div` - margin-top: 10px; - text-transform: uppercase; - color: rgba(0, 0, 0, 0.5); - align-self: center; -` - -const TorrentCache = memo( - ({ cache, isMini }) => { - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) - 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 miniCacheMaxHeight = 340 - - 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 || 0 - const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow) - const activeId = null - - const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ isComplete, inProgress }) => inProgress || isComplete) - const extraEmptyBlocksForFillingLine = - cacheMapWithoutEmptyBlocks.length < amountOfBlocksToRenderInShortView - ? new Array(amountOfBlocksToRenderInShortView - cacheMapWithoutEmptyBlocks.length + 1).fill({}) - : [] - const shortCacheMap = [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine] - - return ( - setDimensions(bounds)}> - {({ measureRef }) => ( -
- - - - {isMini - ? shortCacheMap.map(({ percentage, isComplete, inProgress, isActive, isReaderRange }, i) => { - const currentRow = Math.floor(i / piecesInOneRow) - const shouldBeRendered = inProgress || isComplete || i <= amountOfBlocksToRenderInShortView - - return ( - shouldBeRendered && ( - - ) - ) - }) - : cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => { - const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow) - - return ( - - ) - })} - - - - - {isMini && - (stageOffset + blockSizeWithMargin * amountOfRows || 0) >= miniCacheMaxHeight && - dimensions.height >= miniCacheMaxHeight && scroll down} -
- )} -
- ) - }, - (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/TorrentCache/DefaultSnake.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/DefaultSnake.jsx new file mode 100644 index 0000000..0937ff2 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/DefaultSnake.jsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react' +import DialogContent from '@material-ui/core/DialogContent' +import { Stage, Layer } from 'react-konva' +import Measure from 'react-measure' +import { v4 as uuidv4 } from 'uuid' +import styled from 'styled-components' + +import SingleBlock from './SingleBlock' +import getShortCacheMap from './getShortCacheMap' + +const ScrollNotification = styled.div` + margin-top: 10px; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.5); + align-self: center; +` + +export default function DefaultSnake({ isMini, cacheMap, preloadPiecesAmount }) { + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + const [stageSettings, setStageSettings] = useState({ + boxHeight: null, + strokeWidth: null, + marginBetweenBlocks: null, + stageOffset: null, + }) + 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 miniCacheMaxHeight = 340 + + const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings + + const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks + const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin) + + const shortCacheMap = isMini ? getShortCacheMap({ cacheMap, preloadPiecesAmount, piecesInOneRow }) : [] + + const amountOfRows = Math.ceil((isMini ? shortCacheMap.length : cacheMap.length) / piecesInOneRow) + + const getItemCoordinates = blockOrder => { + const currentRow = Math.floor(blockOrder / piecesInOneRow) + const x = (blockOrder % piecesInOneRow) * blockSizeWithMargin || 0 + const y = currentRow * blockSizeWithMargin || 0 + + return { x, y } + } + + return ( + setDimensions(bounds)}> + {({ measureRef }) => ( +
+ + + + {isMini + ? shortCacheMap.map(({ percentage, isComplete, inProgress, isActive, isReaderRange }, i) => { + const { x, y } = getItemCoordinates(i) + + return ( + + ) + }) + : cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => { + const { x, y } = getItemCoordinates(id) + + return ( + + ) + })} + + + + + {isMini && + (stageOffset + blockSizeWithMargin * amountOfRows || 0) >= miniCacheMaxHeight && + dimensions.height >= miniCacheMaxHeight && scroll down} +
+ )} +
+ ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx new file mode 100644 index 0000000..e2d8dbe --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx @@ -0,0 +1,58 @@ +import { FixedSizeGrid as Grid } from 'react-window' +import AutoSizer from 'react-virtualized-auto-sizer' + +import { getLargeSnakeColors } from './colors' + +const Cell = ({ columnIndex, rowIndex, style, data }) => { + const { columnCount, cacheMap, gutterSize, borderSize, pieces } = data + const itemIndex = rowIndex * columnCount + columnIndex + + const { borderColor, backgroundColor } = getLargeSnakeColors(cacheMap[itemIndex] || {}) + + const newStyle = { + ...style, + left: style.left + gutterSize, + top: style.top + gutterSize, + width: style.width - gutterSize, + height: style.height - gutterSize, + border: `${borderSize}px solid ${borderColor}`, + display: itemIndex >= pieces ? 'none' : null, + background: backgroundColor, + } + + return
+} + +const gutterSize = 2 +const borderSize = 1 +const pieceSize = 12 +const pieceSizeWithSpacing = pieceSize + gutterSize + +export default function LargeSnake({ cacheMap }) { + const pieces = cacheMap.length + + return ( +
+ + {({ height, width }) => { + const columnCount = Math.floor(width / (gutterSize + pieceSize)) - 1 + const rowCount = pieces / columnCount + 1 + + return ( + + {Cell} + + ) + }} + +
+ ) +} diff --git a/web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/SingleBlock.jsx similarity index 72% rename from web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx rename to web/src/components/DialogTorrentDetailsContent/TorrentCache/SingleBlock.jsx index 96268f6..3951532 100644 --- a/web/src/components/DialogTorrentDetailsContent/SingleBlock.jsx +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/SingleBlock.jsx @@ -1,5 +1,7 @@ import { Rect } from 'react-konva' +import { activeColor, completeColor, defaultBorderColor, progressColor, rangeColor } from './colors' + export default function SingleBlock({ x, y, @@ -12,17 +14,17 @@ export default function SingleBlock({ strokeWidth, }) { const strokeColor = isActive - ? '#000' + ? activeColor : isComplete - ? '#3fb57a' + ? completeColor : inProgress - ? '#00d0d0' + ? progressColor : isReaderRange - ? '#9a9aff' - : '#eef2f4' - const backgroundColor = inProgress ? '#00d0d0' : '#eef2f4' - const percentageProgressColor = '#3fb57a' - const processCompletedColor = '#3fb57a' + ? rangeColor + : defaultBorderColor + const backgroundColor = inProgress ? progressColor : defaultBorderColor + const percentageProgressColor = completeColor + const processCompletedColor = completeColor return ( { + const gradientBackgroundColor = inProgress ? progressColor : defaultBackgroundColor + const gradient = `linear-gradient(to top, ${completeColor} 0%, ${completeColor} ${ + percentage * 100 + }%, ${gradientBackgroundColor} ${percentage * 100}%, ${gradientBackgroundColor} 100%)` + + const borderColor = isActive + ? activeColor + : isComplete + ? completeColor + : inProgress + ? progressColor + : isReaderRange + ? rangeColor + : defaultBorderColor + const backgroundColor = isComplete ? completeColor : inProgress ? gradient : defaultBackgroundColor + + return { borderColor, backgroundColor } +} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js new file mode 100644 index 0000000..729c643 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js @@ -0,0 +1,27 @@ +export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => { + const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ isComplete, inProgress }) => inProgress || isComplete) + + const getFullAmountOfBlocks = amountOfBlocks => + // this function counts existed amount of blocks with extra "empty blocks" to fill the row till the end + amountOfBlocks % piecesInOneRow === 0 + ? amountOfBlocks - 1 + : amountOfBlocks + piecesInOneRow - (amountOfBlocks % piecesInOneRow) - 1 || 0 + + const amountOfBlocksToRenderInShortView = getFullAmountOfBlocks(preloadPiecesAmount) + // preloadPiecesAmount is counted from "cache.Capacity / cache.PiecesLength". We always show at least this amount of blocks + const scalableAmountOfBlocksToRenderInShortView = getFullAmountOfBlocks(cacheMapWithoutEmptyBlocks.length) + // cacheMap can become bigger than preloadPiecesAmount counted before. In that case we count blocks dynamically + + const finalAmountOfBlocksToRenderInShortView = Math.max( + // this check is needed to decide which is the biggest amount of blocks and take it to render + scalableAmountOfBlocksToRenderInShortView, + amountOfBlocksToRenderInShortView, + ) + + const extraBlocksAmount = finalAmountOfBlocksToRenderInShortView - cacheMapWithoutEmptyBlocks.length + 1 + // amount of blocks needed to fill the line till the end + + const extraEmptyBlocksForFillingLine = extraBlocksAmount ? new Array(extraBlocksAmount).fill({}) : [] + + return [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine] +} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx new file mode 100644 index 0000000..0938fac --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx @@ -0,0 +1,26 @@ +import { memo } from 'react' +import isEqual from 'lodash/isEqual' + +import { useCreateCacheMap } from '../customHooks' +import LargeSnake from './LargeSnake' +import DefaultSnake from './DefaultSnake' + +const TorrentCache = memo( + ({ cache, isMini }) => { + const cacheMap = useCreateCacheMap(cache) + + const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) + const isSnakeLarge = cacheMap.length > 7000 + + return isMini ? ( + + ) : isSnakeLarge ? ( + + ) : ( + + ) + }, + (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/style.js b/web/src/components/DialogTorrentDetailsContent/style.js index 69beb40..f134e01 100644 --- a/web/src/components/DialogTorrentDetailsContent/style.js +++ b/web/src/components/DialogTorrentDetailsContent/style.js @@ -47,15 +47,13 @@ export const Poster = styled.div` } @media (max-width: 840px) { - height: 200px; - - ${!poster && - css` - width: 150px; - svg { - transform: translateY(-3px); - } - `} + ${poster + ? css` + height: 200px; + ` + : css` + display: none; + `} } `} ` diff --git a/web/src/components/DialogTorrentDetailsContent/widgets.jsx b/web/src/components/DialogTorrentDetailsContent/widgets.jsx index 6a59f0c..991b0a5 100644 --- a/web/src/components/DialogTorrentDetailsContent/widgets.jsx +++ b/web/src/components/DialogTorrentDetailsContent/widgets.jsx @@ -34,7 +34,7 @@ export const UploadSpeedWidget = ({ data }) => ( export const PeersWidget = ({ data }) => ( =3.1.1 <6": + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -10419,6 +10424,19 @@ react-transition-group@^4.4.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtualized-auto-sizer@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.5.tgz#9eeeb8302022de56fbd7a860b08513120ce36509" + integrity sha512-kivjYVWX15TX2IUrm8F1jaCEX8EXrpy3DD+u41WGqJ1ZqbljWpiwscV+VxOM1l7sSIM1jwi2LADjhhAJkJ9dxA== + +react-window@^1.8.6: + version "1.8.6" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" + integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"