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"