diff --git a/web/src/components/App/index.jsx b/web/src/components/App/index.jsx index df8c6b6..8cb3f56 100644 --- a/web/src/components/App/index.jsx +++ b/web/src/components/App/index.jsx @@ -96,6 +96,7 @@ export default function App() { + + + {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} + {!JSON.parse(localStorage.getItem('snackbarIsClosed')) && } diff --git a/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx index cccdf9d..926712e 100644 --- a/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx @@ -29,6 +29,7 @@ export default function DetailedView({ <> {t('Data')} + diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js index 9aed090..efcac57 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/getShortCacheMap.js @@ -1,7 +1,5 @@ export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => { - const cacheMapWithoutEmptyBlocks = cacheMap.filter( - ({ className }) => className.includes('piece-complete') || className.includes('piece-loading'), - ) + const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ percentage }) => percentage > 0) const getFullAmountOfBlocks = amountOfBlocks => // this function counts existed amount of blocks with extra "empty blocks" to fill the row till the end @@ -23,9 +21,7 @@ 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({ className: 'piece' }) - : [] + 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 index 6a4903f..993edfc 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/index.jsx @@ -1,55 +1,110 @@ import Measure from 'react-measure' -import { useState, memo } from 'react' -import { v4 as uuidv4 } from 'uuid' +import { useState, memo, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import isEqual from 'lodash/isEqual' import { useCreateCacheMap } from '../customHooks' -import { gapBetweenPieces, miniCacheMaxHeight, pieceSizeForMiniMap, defaultPieceSize } from './snakeSettings' import getShortCacheMap from './getShortCacheMap' -import { SnakeWrapper, PercentagePiece, ScrollNotification } from './style' +import { SnakeWrapper, ScrollNotification } from './style' +import { readerColor, rangeColor, createGradient, snakeSettings } from './snakeSettings' const TorrentCache = ({ cache, isMini }) => { const { t } = useTranslation() const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + const { width } = dimensions + const canvasRef = useRef(null) + const ctxRef = useRef(null) const cacheMap = useCreateCacheMap(cache) + const settingsTarget = isMini ? 'mini' : 'default' + const { borderWidth, pieceSize, gapBetweenPieces, backgroundColor, borderColor, cacheMaxHeight, completeColor } = + snakeSettings[settingsTarget] - const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) + const canvasWidth = isMini ? width * 0.93 : width - const pieceSize = isMini ? pieceSizeForMiniMap : defaultPieceSize + const pieceSizeWithGap = pieceSize + gapBetweenPieces + const piecesInOneRow = Math.floor(canvasWidth / pieceSizeWithGap) - let piecesInOneRow let shotCacheMap if (isMini) { - const pieceSizeWithGap = pieceSize + gapBetweenPieces - piecesInOneRow = Math.floor((dimensions.width * 0.95) / pieceSizeWithGap) - shotCacheMap = isMini && getShortCacheMap({ cacheMap, preloadPiecesAmount, piecesInOneRow }) + const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1) + shotCacheMap = getShortCacheMap({ cacheMap, preloadPiecesAmount, piecesInOneRow }) } + const source = isMini ? shotCacheMap : cacheMap + const startingXPoint = Math.ceil((canvasWidth - pieceSizeWithGap * piecesInOneRow) / 2) // needed to center grid + const height = Math.ceil(source.length / piecesInOneRow) * pieceSizeWithGap - return isMini ? ( + useEffect(() => { + if (!canvasWidth || !height) return + + const canvas = canvasRef.current + canvas.width = canvasWidth + canvas.height = height + ctxRef.current = canvas.getContext('2d') + }, [canvasRef, height, canvasWidth]) + + useEffect(() => { + const ctx = ctxRef.current + if (!ctx) return + + ctx.clearRect(0, 0, canvasWidth, height) + + source.forEach(({ percentage, isReader, isReaderRange }, i) => { + const inProgress = percentage > 0 && percentage < 100 + const isCompleted = percentage === 100 + const currentRow = i % piecesInOneRow + const currentColumn = Math.floor(i / piecesInOneRow) + const fixBlurStroke = borderWidth % 2 === 0 ? 0 : 0.5 + const requiredFix = Math.ceil(borderWidth / 2) + 1 + fixBlurStroke + const x = currentRow * pieceSize + currentRow * gapBetweenPieces + startingXPoint + requiredFix + const y = currentColumn * pieceSize + currentColumn * gapBetweenPieces + requiredFix + + ctx.lineWidth = borderWidth + ctx.fillStyle = inProgress + ? createGradient(ctx, percentage, settingsTarget) + : isCompleted + ? completeColor + : backgroundColor + ctx.strokeStyle = isReader + ? readerColor + : inProgress || isCompleted + ? completeColor + : isReaderRange + ? rangeColor + : borderColor + + ctx.translate(x, y) + ctx.fillRect(0, 0, pieceSize, pieceSize) + ctx.strokeRect(0, 0, pieceSize, pieceSize) + ctx.setTransform(1, 0, 0, 1, 0, 0) + }) + }, [ + cacheMap, + height, + canvasWidth, + piecesInOneRow, + startingXPoint, + pieceSize, + gapBetweenPieces, + source, + backgroundColor, + borderColor, + borderWidth, + settingsTarget, + completeColor, + ]) + + return ( setDimensions(bounds)}> {({ measureRef }) => ( -
- - {shotCacheMap.map(({ className, id, percentage }) => ( - - {percentage > 0 && percentage <= 100 && } - - ))} +
+ + - {dimensions.height >= miniCacheMaxHeight && {t('ScrollDown')}} + {isMini && height >= cacheMaxHeight && {t('ScrollDown')}}
)} - ) : ( - - {cacheMap.map(({ className, id, percentage }) => ( - - {percentage > 0 && percentage <= 100 && } - - ))} - ) } diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js index 4621717..9db95ac 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js @@ -1,12 +1,35 @@ -export const borderWidth = 1 -export const defaultPieceSize = 14 -export const pieceSizeForMiniMap = 23 -export const gapBetweenPieces = 3 -export const miniCacheMaxHeight = 340 +export const readerColor = '#000' +export const rangeColor = '#afa6e3' -export const defaultBorderColor = '#dbf2e8' -export const defaultBackgroundColor = '#fff' -export const completeColor = '#00a572' -export const progressColor = '#ffa724' -export const activeColor = '#000' -export const rangeColor = '#ffa724' +export const snakeSettings = { + default: { + borderWidth: 1, + pieceSize: 14, + gapBetweenPieces: 3, + backgroundColor: '#fff', + borderColor: '#dbf2e8', + completeColor: '#00a572', + progressColor: '#b3dfc9', + }, + mini: { + cacheMaxHeight: 340, + borderWidth: 2, + pieceSize: 23, + gapBetweenPieces: 6, + backgroundColor: '#dbf2e8', + borderColor: '#6cc196', + completeColor: '#4db380', + progressColor: '#b3dfc9', + }, +} + +export const createGradient = (ctx, percentage, snakeType) => { + const { pieceSize, completeColor, progressColor } = snakeSettings[snakeType] + const gradient = ctx.createLinearGradient(0, pieceSize, 0, 0) + gradient.addColorStop(0, completeColor) + gradient.addColorStop(percentage / 100, completeColor) + gradient.addColorStop(percentage / 100, progressColor) + gradient.addColorStop(1, progressColor) + + return gradient +} diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js index 3887f5d..ace99fc 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js @@ -1,16 +1,6 @@ import styled, { css } from 'styled-components' -import { - defaultBackgroundColor, - defaultBorderColor, - progressColor, - completeColor, - activeColor, - rangeColor, - gapBetweenPieces, - miniCacheMaxHeight, - borderWidth, -} from './snakeSettings' +import { snakeSettings } from './snakeSettings' export const ScrollNotification = styled.div` margin-top: 10px; @@ -20,47 +10,17 @@ export const ScrollNotification = styled.div` ` 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 && + ${({ isMini }) => css` + ${isMini && css` - max-height: ${miniCacheMaxHeight}px; + display: grid; + justify-content: center; + max-height: ${snakeSettings.mini.cacheMaxHeight}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}; + canvas { + display: block; } `} ` - -export const PercentagePiece = styled.div` - background: ${completeColor}; - height: ${({ percentage }) => percentage}%; -` diff --git a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx index 6c50096..1f0cf53 100644 --- a/web/src/components/DialogTorrentDetailsContent/customHooks.jsx +++ b/web/src/components/DialogTorrentDetailsContent/customHooks.jsx @@ -43,25 +43,15 @@ export const useCreateCacheMap = cache => { const map = [] for (let i = 0; i < PiecesCount; i++) { - const newPiece = { id: i } + const { Size, Length } = Pieces[i] || {} - 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) - } + const newPiece = { id: i, percentage: (Size / Length) * 100 || 0 } Readers.forEach(r => { - if (i === r.Reader) { - className.push('piece-reader') - } else if (i >= r.Start && i <= r.End) className.push('reader-range') + if (i === r.Reader) newPiece.isReader = true + if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true }) - newPiece.className = className.join(' ') - map.push(newPiece) } setCacheMap(map) diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx index 0138ddd..8a4140f 100644 --- a/web/src/components/DialogTorrentDetailsContent/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -162,7 +162,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { ) : ( <> {getParsedTitle()} - {ptt.parse(name).title} + {ptt.parse(name || '')?.title} ) ) : (