diff --git a/web/package.json b/web/package.json index ce5efa6..1dbb14a 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,6 @@ "fontsource-roboto": "^4.0.0", "i18next": "^20.3.1", "i18next-browser-languagedetector": "^6.1.1", - "konva": "^8.0.1", "lodash": "^4.17.21", "material-ui-image": "^3.3.2", "parse-torrent": "^9.1.3", @@ -21,12 +20,9 @@ "react-dom": "^17.0.2", "react-dropzone": "^11.3.2", "react-i18next": "^11.10.0", - "react-konva": "^17.0.2-4", "react-measure": "^2.5.2", "react-query": "^3.17.0", "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/DefaultSnake.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/DefaultSnake.jsx deleted file mode 100644 index ada28a9..0000000 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/DefaultSnake.jsx +++ /dev/null @@ -1,127 +0,0 @@ -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 { useTranslation } from 'react-i18next' - -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 { t } = useTranslation() - 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 && {t('ScrollDown')}} -
- )} -
- ) -} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx deleted file mode 100644 index 82e1d56..0000000 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/LargeSnake.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { FixedSizeGrid as Grid } from 'react-window' -import AutoSizer from 'react-virtualized-auto-sizer' -import { memo } from 'react' - -import { getLargeSnakeColors } from './colors' - -const Cell = memo(({ 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/TorrentCache/SingleBlock.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/SingleBlock.jsx deleted file mode 100644 index 3951532..0000000 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/SingleBlock.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Rect } from 'react-konva' - -import { activeColor, completeColor, defaultBorderColor, progressColor, rangeColor } from './colors' - -export default function SingleBlock({ - x, - y, - percentage, - isActive = false, - inProgress = false, - isReaderRange = false, - isComplete = false, - boxHeight, - strokeWidth, -}) { - const strokeColor = isActive - ? activeColor - : isComplete - ? completeColor - : inProgress - ? progressColor - : isReaderRange - ? rangeColor - : defaultBorderColor - const backgroundColor = inProgress ? progressColor : defaultBorderColor - const percentageProgressColor = completeColor - const processCompletedColor = completeColor - - return ( - - ) -} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js deleted file mode 100644 index 235be1d..0000000 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/colors.js +++ /dev/null @@ -1,26 +0,0 @@ -export const defaultBorderColor = '#eef2f4' -export const defaultBackgroundColor = '#fff' -export const completeColor = '#00a572' -export const progressColor = '#ffa724' -export const activeColor = '#000' -export const rangeColor = '#9a9aff' - -export const getLargeSnakeColors = ({ isActive, isComplete, inProgress, isReaderRange, percentage }) => { - 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 index 729c643..9aed090 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js @@ -1,5 +1,7 @@ export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => { - const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ isComplete, inProgress }) => inProgress || isComplete) + const cacheMapWithoutEmptyBlocks = cacheMap.filter( + ({ className }) => className.includes('piece-complete') || className.includes('piece-loading'), + ) const getFullAmountOfBlocks = amountOfBlocks => // this function counts existed amount of blocks with extra "empty blocks" to fill the row till the end @@ -21,7 +23,9 @@ export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => { const extraBlocksAmount = finalAmountOfBlocksToRenderInShortView - cacheMapWithoutEmptyBlocks.length + 1 // amount of blocks needed to fill the line till the end - const extraEmptyBlocksForFillingLine = extraBlocksAmount ? new Array(extraBlocksAmount).fill({}) : [] + const extraEmptyBlocksForFillingLine = extraBlocksAmount + ? new Array(extraBlocksAmount).fill({ className: 'piece' }) + : [] return [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine] } diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx b/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx index 10c75fd..6a4903f 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx @@ -1,26 +1,59 @@ -import { memo } from 'react' +import Measure from 'react-measure' +import { useState, memo } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { useTranslation } from 'react-i18next' import isEqual from 'lodash/isEqual' import { useCreateCacheMap } from '../customHooks' -import LargeSnake from './LargeSnake' -import DefaultSnake from './DefaultSnake' +import { gapBetweenPieces, miniCacheMaxHeight, pieceSizeForMiniMap, defaultPieceSize } from './snakeSettings' +import getShortCacheMap from './getShortCacheMap' +import { SnakeWrapper, PercentagePiece, ScrollNotification } from './style' -const TorrentCache = memo( - ({ cache, isMini }) => { - const cacheMap = useCreateCacheMap(cache) +const TorrentCache = ({ cache, isMini }) => { + const { t } = useTranslation() + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + const cacheMap = useCreateCacheMap(cache) - const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) - const isSnakeLarge = cacheMap.length > 1000 + const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) - return isMini ? ( - - ) : isSnakeLarge ? ( - - ) : ( - - ) - }, + const pieceSize = isMini ? pieceSizeForMiniMap : defaultPieceSize + + let piecesInOneRow + let shotCacheMap + if (isMini) { + const pieceSizeWithGap = pieceSize + gapBetweenPieces + piecesInOneRow = Math.floor((dimensions.width * 0.95) / pieceSizeWithGap) + shotCacheMap = isMini && getShortCacheMap({ cacheMap, preloadPiecesAmount, piecesInOneRow }) + } + + return isMini ? ( + setDimensions(bounds)}> + {({ measureRef }) => ( +
+ + {shotCacheMap.map(({ className, id, percentage }) => ( + + {percentage > 0 && percentage <= 100 && } + + ))} + + + {dimensions.height >= miniCacheMaxHeight && {t('ScrollDown')}} +
+ )} +
+ ) : ( + + {cacheMap.map(({ className, id, percentage }) => ( + + {percentage > 0 && percentage <= 100 && } + + ))} + + ) +} + +export default memo( + TorrentCache, (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/snakeSettings.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js new file mode 100644 index 0000000..82c4463 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js @@ -0,0 +1,12 @@ +export const borderWidth = 1 +export const defaultPieceSize = 14 +export const pieceSizeForMiniMap = 23 +export const gapBetweenPieces = 3 +export const miniCacheMaxHeight = 340 + +export const defaultBorderColor = '#eef2f4' +export const defaultBackgroundColor = '#fff' +export const completeColor = '#00a572' +export const progressColor = '#ffa724' +export const activeColor = '#000' +export const rangeColor = '#9a9aff' diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js new file mode 100644 index 0000000..532a6d3 --- /dev/null +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js @@ -0,0 +1,66 @@ +import styled, { css } from 'styled-components' + +import { + defaultBackgroundColor, + defaultBorderColor, + progressColor, + completeColor, + activeColor, + rangeColor, + gapBetweenPieces, + miniCacheMaxHeight, + borderWidth, +} from './snakeSettings' + +export const ScrollNotification = styled.div` + margin-top: 10px; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.5); + align-self: center; +` + +export const SnakeWrapper = styled.div` + ${({ pieceSize, piecesInOneRow }) => css` + display: grid; + gap: ${gapBetweenPieces}px; + grid-template-columns: repeat(${piecesInOneRow || 'auto-fit'}, ${pieceSize}px); + grid-auto-rows: max-content; + justify-content: center; + + ${piecesInOneRow && + css` + max-height: ${miniCacheMaxHeight}px; + overflow: auto; + `} + + .piece { + width: ${pieceSize}px; + height: ${pieceSize}px; + background: ${defaultBackgroundColor}; + border: ${borderWidth}px solid ${defaultBorderColor}; + display: grid; + align-items: end; + + &-loading { + background: ${progressColor}; + border-color: ${progressColor}; + } + &-complete { + background: ${completeColor}; + border-color: ${completeColor}; + } + &-reader { + border-color: ${activeColor}; + } + } + + .reader-range { + border-color: ${rangeColor}; + } + `} +` + +export const PercentagePiece = styled.div` + background: ${completeColor}; + height: ${({ percentage }) => (percentage / 100) * 12}px; +` diff --git a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx index 5eeed67..6c50096 100644 --- a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx +++ b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx @@ -38,32 +38,32 @@ export const useCreateCacheMap = cache => { const [cacheMap, setCacheMap] = useState([]) useEffect(() => { - if (!cache.PiecesCount || !cache.Pieces) return - - const { Pieces, PiecesCount, Readers } = cache + const { PiecesCount, Pieces, 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) - } + const activeBlock = Pieces[i] + const className = ['piece'] + + if (activeBlock) { + const { Completed, Size, Length } = activeBlock + className.push(Completed && Size >= Length ? 'piece-complete' : 'piece-loading') + newPiece.percentage = ((Size / Length) * 100).toFixed(2) } Readers.forEach(r => { - if (i === r.Reader) newPiece.isActive = true - if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true + if (i === r.Reader) { + className.push('piece-reader') + } else if (i >= r.Start && i <= r.End) className.push('reader-range') }) + newPiece.className = className.join(' ') + map.push(newPiece) } - setCacheMap(map) }, [cache]) diff --git a/web/yarn.lock b/web/yarn.lock index 4e7b69f..34ff6b6 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1174,7 +1174,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.14.0", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.14.0", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== @@ -7938,11 +7938,6 @@ klona@^2.0.4: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== -konva@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/konva/-/konva-8.0.1.tgz#f34f483cdf62c36f966addc1a7484ed694313c2b" - integrity sha512-QDppGS1L5Dhod1zjwy9GVVjeyfPBHnPncL5oRh1NyjR1mEvhrLjzflrkdW+p73uFIW9hwCDZVLGxzzjQre9izw== - language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -8306,11 +8301,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -"memoize-one@>=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" @@ -10477,14 +10467,6 @@ react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-konva@^17.0.2-4: - version "17.0.2-4" - resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-17.0.2-4.tgz#afd0968e1295b624bf2a7a154ba294e0d5be55cd" - integrity sha512-YvRVPT81y8sMQV1SY1/tIDetGxBK+7Rk86u4LmiyDBLLE17vD78F01b8EC3AuP3nI3hUaTblfBugUF35cm6Etg== - dependencies: - react-reconciler "~0.26.2" - scheduler "^0.20.2" - react-measure@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.5.2.tgz#4ffc410e8b9cb836d9455a9ff18fc1f0fca67f89" @@ -10504,15 +10486,6 @@ react-query@^3.17.0: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-reconciler@~0.26.2: - version "0.26.2" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" - integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -10594,19 +10567,6 @@ 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"