mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
snake replaced with canvas
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => {
|
export default ({ cacheMap, preloadPiecesAmount, piecesInOneRow }) => {
|
||||||
const cacheMapWithoutEmptyBlocks = cacheMap.filter(
|
const cacheMapWithoutEmptyBlocks = cacheMap.filter(({ percentage }) => percentage > 0)
|
||||||
({ className }) => className.includes('piece-complete') || className.includes('piece-loading'),
|
|
||||||
)
|
|
||||||
|
|
||||||
const getFullAmountOfBlocks = amountOfBlocks =>
|
const getFullAmountOfBlocks = amountOfBlocks =>
|
||||||
// this function counts existed amount of blocks with extra "empty blocks" to fill the row till the end
|
// 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
|
const extraBlocksAmount = finalAmountOfBlocksToRenderInShortView - cacheMapWithoutEmptyBlocks.length + 1
|
||||||
// amount of blocks needed to fill the line till the end
|
// amount of blocks needed to fill the line till the end
|
||||||
|
|
||||||
const extraEmptyBlocksForFillingLine = extraBlocksAmount
|
const extraEmptyBlocksForFillingLine = extraBlocksAmount ? new Array(extraBlocksAmount).fill({}) : []
|
||||||
? new Array(extraBlocksAmount).fill({ className: 'piece' })
|
|
||||||
: []
|
|
||||||
|
|
||||||
return [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine]
|
return [...cacheMapWithoutEmptyBlocks, ...extraEmptyBlocksForFillingLine]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,113 @@
|
|||||||
import Measure from 'react-measure'
|
import Measure from 'react-measure'
|
||||||
import { useState, memo } from 'react'
|
import { useState, memo, useRef, useEffect } from 'react'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
|
|
||||||
import { useCreateCacheMap } from '../customHooks'
|
import { useCreateCacheMap } from '../customHooks'
|
||||||
import { gapBetweenPieces, miniCacheMaxHeight, pieceSizeForMiniMap, defaultPieceSize } from './snakeSettings'
|
|
||||||
import getShortCacheMap from './getShortCacheMap'
|
import getShortCacheMap from './getShortCacheMap'
|
||||||
import { SnakeWrapper, PercentagePiece, ScrollNotification } from './style'
|
import { SnakeWrapper, ScrollNotification } from './style'
|
||||||
|
import {
|
||||||
|
defaultBorderWidth,
|
||||||
|
miniCacheMaxHeight,
|
||||||
|
pieceSizeForMiniMap,
|
||||||
|
defaultPieceSize,
|
||||||
|
defaultBackgroundColor,
|
||||||
|
defaultBorderColor,
|
||||||
|
completeColor,
|
||||||
|
activeColor,
|
||||||
|
rangeColor,
|
||||||
|
defaultGapBetweenPieces,
|
||||||
|
miniBackgroundColor,
|
||||||
|
miniBorderWidth,
|
||||||
|
miniGapBetweenPieces,
|
||||||
|
createGradient,
|
||||||
|
} from './snakeSettings'
|
||||||
|
|
||||||
const TorrentCache = ({ cache, isMini }) => {
|
const TorrentCache = ({ cache, isMini }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||||
|
const { width } = dimensions
|
||||||
|
const canvasRef = useRef(null)
|
||||||
|
const ctxRef = useRef(null)
|
||||||
const cacheMap = useCreateCacheMap(cache)
|
const cacheMap = useCreateCacheMap(cache)
|
||||||
|
|
||||||
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
|
const canvasWidth = isMini ? width * 0.93 : width
|
||||||
|
|
||||||
const pieceSize = isMini ? pieceSizeForMiniMap : defaultPieceSize
|
const pieceSize = isMini ? pieceSizeForMiniMap : defaultPieceSize
|
||||||
|
const gapBetweenPieces = isMini ? miniGapBetweenPieces : defaultGapBetweenPieces
|
||||||
|
|
||||||
|
const pieceSizeWithGap = pieceSize + gapBetweenPieces
|
||||||
|
const piecesInOneRow = Math.floor(canvasWidth / pieceSizeWithGap)
|
||||||
|
|
||||||
let piecesInOneRow
|
|
||||||
let shotCacheMap
|
let shotCacheMap
|
||||||
if (isMini) {
|
if (isMini) {
|
||||||
const pieceSizeWithGap = pieceSize + gapBetweenPieces
|
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
|
||||||
piecesInOneRow = Math.floor((dimensions.width * 0.95) / pieceSizeWithGap)
|
shotCacheMap = getShortCacheMap({ cacheMap, preloadPiecesAmount, piecesInOneRow })
|
||||||
shotCacheMap = isMini && 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 borderWidth = isMini ? miniBorderWidth : defaultBorderWidth
|
||||||
|
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)
|
||||||
|
: isCompleted
|
||||||
|
? completeColor
|
||||||
|
: isMini
|
||||||
|
? miniBackgroundColor
|
||||||
|
: defaultBackgroundColor
|
||||||
|
ctx.strokeStyle = isReader
|
||||||
|
? activeColor
|
||||||
|
: inProgress || isCompleted
|
||||||
|
? completeColor
|
||||||
|
: isReaderRange
|
||||||
|
? rangeColor
|
||||||
|
: defaultBorderColor
|
||||||
|
|
||||||
|
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, isMini, startingXPoint, pieceSize, gapBetweenPieces, source])
|
||||||
|
|
||||||
|
return (
|
||||||
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
||||||
{({ measureRef }) => (
|
{({ measureRef }) => (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }} ref={measureRef}>
|
||||||
<SnakeWrapper ref={measureRef} pieceSize={pieceSize} piecesInOneRow={piecesInOneRow}>
|
<SnakeWrapper isMini={isMini}>
|
||||||
{shotCacheMap.map(({ className, id, percentage }) => (
|
<canvas ref={canvasRef} />
|
||||||
<span key={id || uuidv4()} className={className}>
|
|
||||||
{percentage > 0 && percentage <= 100 && <PercentagePiece percentage={percentage} />}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</SnakeWrapper>
|
</SnakeWrapper>
|
||||||
|
|
||||||
{dimensions.height >= miniCacheMaxHeight && <ScrollNotification>{t('ScrollDown')}</ScrollNotification>}
|
{isMini && height >= miniCacheMaxHeight && <ScrollNotification>{t('ScrollDown')}</ScrollNotification>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Measure>
|
</Measure>
|
||||||
) : (
|
|
||||||
<SnakeWrapper pieceSize={pieceSize}>
|
|
||||||
{cacheMap.map(({ className, id, percentage }) => (
|
|
||||||
<span key={id || uuidv4()} className={className}>
|
|
||||||
{percentage > 0 && percentage <= 100 && <PercentagePiece percentage={percentage} />}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</SnakeWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
export const borderWidth = 1
|
import { cacheBackground } from '../style'
|
||||||
|
|
||||||
|
export const defaultBorderWidth = 1
|
||||||
|
export const miniBorderWidth = 2
|
||||||
export const defaultPieceSize = 14
|
export const defaultPieceSize = 14
|
||||||
export const pieceSizeForMiniMap = 23
|
export const pieceSizeForMiniMap = 23
|
||||||
export const gapBetweenPieces = 3
|
export const defaultGapBetweenPieces = 3
|
||||||
|
export const miniGapBetweenPieces = 6
|
||||||
export const miniCacheMaxHeight = 340
|
export const miniCacheMaxHeight = 340
|
||||||
|
|
||||||
export const defaultBorderColor = '#dbf2e8'
|
export const defaultBorderColor = '#dbf2e8'
|
||||||
export const defaultBackgroundColor = '#fff'
|
export const defaultBackgroundColor = '#fff'
|
||||||
|
export const miniBackgroundColor = cacheBackground
|
||||||
export const completeColor = '#00a572'
|
export const completeColor = '#00a572'
|
||||||
export const progressColor = '#ffa724'
|
export const progressColor = '#86beee'
|
||||||
export const activeColor = '#000'
|
export const activeColor = '#000'
|
||||||
export const rangeColor = '#ffa724'
|
export const rangeColor = '#afa6e3'
|
||||||
|
|
||||||
|
export const createGradient = (ctx, percentage) => {
|
||||||
|
const gradient = ctx.createLinearGradient(0, 12, 0, 0)
|
||||||
|
gradient.addColorStop(0, completeColor)
|
||||||
|
gradient.addColorStop(percentage / 100, completeColor)
|
||||||
|
gradient.addColorStop(percentage / 100, progressColor)
|
||||||
|
gradient.addColorStop(1, progressColor)
|
||||||
|
|
||||||
|
return gradient
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
import {
|
import { miniCacheMaxHeight } from './snakeSettings'
|
||||||
defaultBackgroundColor,
|
|
||||||
defaultBorderColor,
|
|
||||||
progressColor,
|
|
||||||
completeColor,
|
|
||||||
activeColor,
|
|
||||||
rangeColor,
|
|
||||||
gapBetweenPieces,
|
|
||||||
miniCacheMaxHeight,
|
|
||||||
borderWidth,
|
|
||||||
} from './snakeSettings'
|
|
||||||
|
|
||||||
export const ScrollNotification = styled.div`
|
export const ScrollNotification = styled.div`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -20,47 +10,17 @@ export const ScrollNotification = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const SnakeWrapper = styled.div`
|
export const SnakeWrapper = styled.div`
|
||||||
${({ pieceSize, piecesInOneRow }) => css`
|
${({ isMini }) => css`
|
||||||
display: grid;
|
${isMini &&
|
||||||
gap: ${gapBetweenPieces}px;
|
|
||||||
grid-template-columns: repeat(${piecesInOneRow || 'auto-fit'}, ${pieceSize}px);
|
|
||||||
grid-auto-rows: max-content;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
${piecesInOneRow &&
|
|
||||||
css`
|
css`
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
max-height: ${miniCacheMaxHeight}px;
|
max-height: ${miniCacheMaxHeight}px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
`}
|
`}
|
||||||
|
|
||||||
.piece {
|
canvas {
|
||||||
width: ${pieceSize}px;
|
display: block;
|
||||||
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}%;
|
|
||||||
`
|
|
||||||
@@ -43,25 +43,15 @@ export const useCreateCacheMap = cache => {
|
|||||||
const map = []
|
const map = []
|
||||||
|
|
||||||
for (let i = 0; i < PiecesCount; i++) {
|
for (let i = 0; i < PiecesCount; i++) {
|
||||||
const newPiece = { id: i }
|
const { Size, Length } = Pieces[i] || {}
|
||||||
|
|
||||||
const activeBlock = Pieces[i]
|
const newPiece = { id: i, percentage: (Size / Length) * 100 || 0 }
|
||||||
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 => {
|
Readers.forEach(r => {
|
||||||
if (i === r.Reader) {
|
if (i === r.Reader) newPiece.isReader = true
|
||||||
className.push('piece-reader')
|
if (i >= r.Start && i <= r.End) newPiece.isReaderRange = true
|
||||||
} else if (i >= r.Start && i <= r.End) className.push('reader-range')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
newPiece.className = className.join(' ')
|
|
||||||
|
|
||||||
map.push(newPiece)
|
map.push(newPiece)
|
||||||
}
|
}
|
||||||
setCacheMap(map)
|
setCacheMap(map)
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SectionTitle>{getParsedTitle()}</SectionTitle>
|
<SectionTitle>{getParsedTitle()}</SectionTitle>
|
||||||
<SectionSubName mb={20}>{ptt.parse(name).title}</SectionSubName>
|
<SectionSubName mb={20}>{ptt.parse(name || '')?.title}</SectionSubName>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -74,13 +74,14 @@ export const MainSection = styled.section`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const cacheBackground = '#88cdaa'
|
||||||
export const CacheSection = styled.section`
|
export const CacheSection = styled.section`
|
||||||
grid-area: cache;
|
grid-area: cache;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
grid-template-rows: min-content 1fr min-content;
|
grid-template-rows: min-content 1fr min-content;
|
||||||
background: #88cdaa;
|
background: ${cacheBackground};
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user