mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
Merge branch 'master' of https://github.com/YouROK/TorrServer
This commit is contained in:
@@ -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 (
|
||||
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
||||
{({ measureRef }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<DialogContent
|
||||
ref={measureRef}
|
||||
{...(isMini
|
||||
? { style: { padding: 0, maxHeight: `${miniCacheMaxHeight}px`, overflow: 'auto' } }
|
||||
: { style: { padding: 0 } })}
|
||||
>
|
||||
<Stage
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
offset={{ x: -stageOffset, y: -stageOffset }}
|
||||
width={stageOffset + blockSizeWithMargin * piecesInOneRow || 0}
|
||||
height={stageOffset + blockSizeWithMargin * amountOfRows || 0}
|
||||
>
|
||||
<Layer>
|
||||
{isMini
|
||||
? shortCacheMap.map(({ percentage, isComplete, inProgress, isActive, isReaderRange }, i) => {
|
||||
const currentRow = Math.floor(i / piecesInOneRow)
|
||||
const shouldBeRendered = inProgress || isComplete || i <= amountOfBlocksToRenderInShortView
|
||||
|
||||
return (
|
||||
shouldBeRendered && (
|
||||
<SingleBlock
|
||||
key={uuidv4()}
|
||||
x={(i % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
)
|
||||
})
|
||||
: cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
|
||||
const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow)
|
||||
|
||||
return (
|
||||
<SingleBlock
|
||||
key={uuidv4()}
|
||||
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</DialogContent>
|
||||
|
||||
{isMini &&
|
||||
(stageOffset + blockSizeWithMargin * amountOfRows || 0) >= miniCacheMaxHeight &&
|
||||
dimensions.height >= miniCacheMaxHeight && <ScrollNotification>scroll down</ScrollNotification>}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
|
||||
)
|
||||
|
||||
export default TorrentCache
|
||||
@@ -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 (
|
||||
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
||||
{({ measureRef }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<DialogContent
|
||||
ref={measureRef}
|
||||
{...(isMini
|
||||
? { style: { padding: 0, maxHeight: `${miniCacheMaxHeight}px`, overflow: 'auto' } }
|
||||
: { style: { padding: 0 } })}
|
||||
>
|
||||
<Stage
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
offset={{ x: -stageOffset, y: -stageOffset }}
|
||||
width={stageOffset + blockSizeWithMargin * piecesInOneRow || 0}
|
||||
height={stageOffset + blockSizeWithMargin * amountOfRows || 0}
|
||||
>
|
||||
<Layer>
|
||||
{isMini
|
||||
? shortCacheMap.map(({ percentage, isComplete, inProgress, isActive, isReaderRange }, i) => {
|
||||
const { x, y } = getItemCoordinates(i)
|
||||
|
||||
return (
|
||||
<SingleBlock
|
||||
key={uuidv4()}
|
||||
x={x}
|
||||
y={y}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
|
||||
const { x, y } = getItemCoordinates(id)
|
||||
|
||||
return (
|
||||
<SingleBlock
|
||||
key={uuidv4()}
|
||||
x={x}
|
||||
y={y}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</DialogContent>
|
||||
|
||||
{isMini &&
|
||||
(stageOffset + blockSizeWithMargin * amountOfRows || 0) >= miniCacheMaxHeight &&
|
||||
dimensions.height >= miniCacheMaxHeight && <ScrollNotification>scroll down</ScrollNotification>}
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
)
|
||||
}
|
||||
@@ -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 <div style={newStyle} />
|
||||
}
|
||||
|
||||
const gutterSize = 2
|
||||
const borderSize = 1
|
||||
const pieceSize = 12
|
||||
const pieceSizeWithSpacing = pieceSize + gutterSize
|
||||
|
||||
export default function LargeSnake({ cacheMap }) {
|
||||
const pieces = cacheMap.length
|
||||
|
||||
return (
|
||||
<div style={{ height: '60vh', overflow: 'hidden' }}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
const columnCount = Math.floor(width / (gutterSize + pieceSize)) - 1
|
||||
const rowCount = pieces / columnCount + 1
|
||||
|
||||
return (
|
||||
<Grid
|
||||
columnCount={columnCount}
|
||||
rowCount={rowCount}
|
||||
columnWidth={pieceSizeWithSpacing}
|
||||
rowHeight={pieceSizeWithSpacing}
|
||||
height={height}
|
||||
width={width}
|
||||
itemData={{ columnCount, cacheMap, gutterSize, borderSize, pieces }}
|
||||
>
|
||||
{Cell}
|
||||
</Grid>
|
||||
)
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Rect
|
||||
@@ -0,0 +1,26 @@
|
||||
export const defaultBorderColor = '#eef2f4'
|
||||
export const defaultBackgroundColor = '#fff'
|
||||
export const completeColor = '#3fb57a'
|
||||
export const progressColor = '#00d0d0'
|
||||
export const activeColor = '#000'
|
||||
export const rangeColor = '#9a9aff'
|
||||
|
||||
export const getLargeSnakeColors = ({ isActive, isComplete, inProgress, isReaderRange, percentage }) => {
|
||||
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 }
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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 ? (
|
||||
<DefaultSnake isMini cacheMap={cacheMap} preloadPiecesAmount={preloadPiecesAmount} />
|
||||
) : isSnakeLarge ? (
|
||||
<LargeSnake cacheMap={cacheMap} />
|
||||
) : (
|
||||
<DefaultSnake cacheMap={cacheMap} preloadPiecesAmount={preloadPiecesAmount} />
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
|
||||
)
|
||||
|
||||
export default TorrentCache
|
||||
@@ -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;
|
||||
`}
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
@@ -34,7 +34,7 @@ export const UploadSpeedWidget = ({ data }) => (
|
||||
export const PeersWidget = ({ data }) => (
|
||||
<StatisticsField
|
||||
title='Peers'
|
||||
value={getPeerString(data)}
|
||||
value={getPeerString(data) || '[0] 0 / 0'}
|
||||
iconBg='#cdc118'
|
||||
valueBg='#d8cb18'
|
||||
icon={SwapVerticalCircleIcon}
|
||||
|
||||
@@ -5,7 +5,7 @@ export function humanizeSize(size) {
|
||||
}
|
||||
|
||||
export function getPeerString(torrent) {
|
||||
if (!torrent || !torrent.connected_seeders) return ''
|
||||
if (!torrent || !torrent.connected_seeders) return null
|
||||
return `[${torrent.connected_seeders}] ${torrent.active_peers} / ${torrent.total_peers}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user