diff --git a/web/package.json b/web/package.json
index 261134a..5c00cee 100644
--- a/web/package.json
+++ b/web/package.json
@@ -7,9 +7,12 @@
"@material-ui/icons": "^4.11.2",
"clsx": "^1.1.1",
"fontsource-roboto": "^4.0.0",
+ "konva": "^8.0.1",
"material-ui-image": "^3.3.2",
"react": "^17.0.2",
"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/src/components/DialogCacheInfo.jsx b/web/src/components/DialogCacheInfo.jsx
deleted file mode 100644
index d64a62d..0000000
--- a/web/src/components/DialogCacheInfo.jsx
+++ /dev/null
@@ -1,190 +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'
-import styled, { css } from 'styled-components'
-
-const boxHeight = 12
-
-const CacheWrapper = styled.div`
- padding-left: 6px;
- padding-right: 2px;
- line-height: 11px;
-
- .piece {
- width: ${boxHeight}px;
- height: ${boxHeight}px;
- background-color: #eef2f4;
- border: 1px solid #eef2f4;
- display: inline-block;
- margin-right: 1px;
- }
- .piece-complete {
- background-color: #3fb57a;
- border-color: #3fb57a;
- }
- .piece-loading {
- background-color: #00d0d0;
- border-color: #00d0d0;
- }
- .reader-range {
- border-color: #9a9aff;
- }
- .piece-reader {
- border-color: #000000;
- }
-`
-
-const PieceInProgress = styled.div`
- ${({ prc }) => css`
- position: relative;
- z-index: 1;
- background-color: #3fb57a;
-
- top: -1px;
- left: -1px;
- width: 12px;
- height: ${prc * boxHeight}px;
- `}
-`
-
-export default function DialogCacheInfo({ hash }) {
- const [cache, setCache] = useState({})
- const [pMap, setPMap] = useState([])
- const timerID = useRef(null)
- const componentIsMounted = useRef(true)
-
- useEffect(
- // 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 cls = ['piece']
- let prc = 0
-
- const currentPiece = Pieces[i]
- if (currentPiece) {
- if (currentPiece.Completed && currentPiece.Size === currentPiece.Length) cls.push('piece-complete')
- else cls.push('piece-loading')
-
- prc = (currentPiece.Size / currentPiece.Length).toFixed(2)
- }
-
- Readers.forEach(r => {
- if (i === r.Reader) return cls.push('piece-reader')
- if (i >= r.Start && i <= r.End) cls.push('reader-range')
- })
-
- map.push({ prc, className: cls.join(' '), id: i })
- }
-
- setPMap(map)
- }, [cache])
-
- 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(({ prc, className: currentPieceCalss, id }) => (
-
- {prc > 0 && prc < 1 && }
-
- ))}
-
-
-
- )
-}
-
-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",
- "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/DialogCacheInfo/SingleBlock.jsx b/web/src/components/DialogCacheInfo/SingleBlock.jsx
new file mode 100644
index 0000000..ba8cb01
--- /dev/null
+++ b/web/src/components/DialogCacheInfo/SingleBlock.jsx
@@ -0,0 +1,56 @@
+import { Rect } from 'react-konva'
+
+export const boxHeight = 12
+export const strokeWidth = 2
+export const marginBetweenBlocks = 2
+
+export default function SingleBlock({
+ x,
+ y,
+ percentage,
+ isActive = false,
+ inProgress = false,
+ isReaderRange = false,
+ isComplete = false,
+}) {
+ 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/DialogCacheInfo/index.jsx b/web/src/components/DialogCacheInfo/index.jsx
new file mode 100644
index 0000000..28946fd
--- /dev/null
+++ b/web/src/components/DialogCacheInfo/index.jsx
@@ -0,0 +1,183 @@
+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'
+import { Stage, Layer } from 'react-konva'
+import Measure from 'react-measure'
+
+import SingleBlock, { boxHeight, strokeWidth, marginBetweenBlocks } 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 })
+
+ useEffect(
+ // 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
+ return
+ }
+ if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true
+ })
+
+ map.push(newPiece)
+ }
+
+ setPMap(map)
+ }, [cache])
+
+ const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks
+ const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin)
+ const amountOfRows = Math.ceil(pMap.length / piecesInOneRow) + 1
+
+ return (
+ setDimensions(contentRect.bounds)}>
+ {({ measureRef }) => (
+
+
+
+ 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.length ? (
+ 'loading'
+ ) : (
+
+
+ {pMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
+ const currentRow = Math.floor(id / piecesInOneRow) + 1
+
+ return (
+
+ )
+ })}
+
+
+ )}
+
+
+ )}
+
+ )
+}
+
+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",
+ "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/yarn.lock b/web/yarn.lock
index d9fe032..162396c 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1174,7 +1174,7 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@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.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==
@@ -5927,6 +5927,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
+get-node-dimensions@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823"
+ integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==
+
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -7826,6 +7831,11 @@ 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"
@@ -10269,6 +10279,33 @@ 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"
+ integrity sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==
+ dependencies:
+ "@babel/runtime" "^7.2.0"
+ get-node-dimensions "^1.2.1"
+ prop-types "^15.6.2"
+ resize-observer-polyfill "^1.5.0"
+
+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"
@@ -10638,6 +10675,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+resize-observer-polyfill@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"