cache blocks rewritten on canvas

This commit is contained in:
Daniel Shleifman
2021-05-26 21:10:26 +03:00
parent e3983fddf7
commit 9373a64490
5 changed files with 285 additions and 191 deletions

View File

@@ -7,9 +7,12 @@
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"konva": "^8.0.1",
"material-ui-image": "^3.3.2", "material-ui-image": "^3.3.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-konva": "^17.0.2-4",
"react-measure": "^2.5.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
}, },

View File

@@ -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 (
<div>
<DialogTitle id='form-dialog-title'>
<Typography>
<b>Hash </b> {cache.Hash}
<br />
<b>Capacity </b> {humanizeSize(cache.Capacity)}
<br />
<b>Filled </b> {humanizeSize(cache.Filled)}
<br />
<b>Torrent size </b> {cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)}
<br />
<b>Pieces length </b> {humanizeSize(cache.PiecesLength)}
<br />
<b>Pieces count </b> {cache.PiecesCount}
<br />
<b>Peers: </b> {getPeerString(cache.Torrent)}
<br />
<b>Download speed </b>{' '}
{cache.Torrent && cache.Torrent.download_speed ? `${humanizeSize(cache.Torrent.download_speed)}/sec` : ''}
<br />
<b>Upload speed </b>{' '}
{cache.Torrent && cache.Torrent.upload_speed ? `${humanizeSize(cache.Torrent.upload_speed)}/sec` : ''}
<br />
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
</Typography>
</DialogTitle>
<DialogContent>
<CacheWrapper>
{pMap.map(({ prc, className: currentPieceCalss, id }) => (
<span key={id} className={currentPieceCalss}>
{prc > 0 && prc < 1 && <PieceInProgress prc={prc} />}
</span>
))}
</CacheWrapper>
</DialogContent>
</div>
)
}
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
}
}
}
*/

View File

@@ -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 (
<Rect
x={x}
y={y}
stroke={strokeColor}
strokeWidth={strokeWidth}
height={boxHeight}
width={boxHeight}
fillAfterStrokeEnabled
preventDefault={false}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isComplete
? { fill: processCompletedColor }
: inProgress && {
fillLinearGradientStartPointY: boxHeight,
fillLinearGradientEndPointY: 0,
fillLinearGradientColorStops: [
0,
percentageProgressColor,
percentage,
percentageProgressColor,
percentage,
backgroundColor,
],
})}
/>
)
}

View File

@@ -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 (
<Measure bounds onResize={contentRect => setDimensions(contentRect.bounds)}>
{({ measureRef }) => (
<div ref={measureRef}>
<DialogTitle id='form-dialog-title'>
<Typography>
<b>Hash </b> <span style={{ wordBreak: 'break-word' }}>{cache.Hash}</span>
<br />
<b>Capacity </b> {humanizeSize(cache.Capacity)}
<br />
<b>Filled </b> {humanizeSize(cache.Filled)}
<br />
<b>Torrent size </b>{' '}
{cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)}
<br />
<b>Pieces length </b> {humanizeSize(cache.PiecesLength)}
<br />
<b>Pieces count </b> {cache.PiecesCount}
<br />
<b>Peers: </b> {getPeerString(cache.Torrent)}
<br />
<b>Download speed </b>{' '}
{cache.Torrent && cache.Torrent.download_speed ? `${humanizeSize(cache.Torrent.download_speed)}/sec` : ''}
<br />
<b>Upload speed </b>{' '}
{cache.Torrent && cache.Torrent.upload_speed ? `${humanizeSize(cache.Torrent.upload_speed)}/sec` : ''}
<br />
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
</Typography>
</DialogTitle>
<DialogContent>
{!pMap.length ? (
'loading'
) : (
<Stage
style={{ display: 'flex', justifyContent: 'center' }}
offsetX={-3}
width={dimensions.width * 0.9}
height={amountOfRows * blockSizeWithMargin}
>
<Layer>
{pMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
const currentRow = Math.floor(id / piecesInOneRow) + 1
return (
<SingleBlock
key={id}
x={(id % piecesInOneRow) * blockSizeWithMargin}
y={currentRow * blockSizeWithMargin}
percentage={percentage}
inProgress={inProgress}
isComplete={isComplete}
isReaderRange={isReaderRange}
isActive={isActive}
/>
)
})}
</Layer>
</Stage>
)}
</DialogContent>
</div>
)}
</Measure>
)
}
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
}
}
}
*/

View File

@@ -1174,7 +1174,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" 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" version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== 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 "^1.0.3"
has-symbols "^1.0.1" 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: get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" 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" resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== 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: language-subtag-registry@~0.3.2:
version "0.3.21" version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" 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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== 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: react-refresh@^0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" 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" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 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: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"