mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
refactor
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"fontsource-roboto": "^4.0.0",
|
"fontsource-roboto": "^4.0.0",
|
||||||
"konva": "^8.0.1",
|
"konva": "^8.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"material-ui-image": "^3.3.2",
|
"material-ui-image": "^3.3.2",
|
||||||
"parse-torrent-title": "^1.3.0",
|
"parse-torrent-title": "^1.3.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|||||||
144
web/src/components/DialogTorrentDetailsContent/Table/index.jsx
Normal file
144
web/src/components/DialogTorrentDetailsContent/Table/index.jsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { streamHost } from 'utils/Hosts'
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
import { humanizeSize } from 'utils/Utils'
|
||||||
|
import ptt from 'parse-torrent-title'
|
||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||||
|
|
||||||
|
import { TableStyle, ShortTableWrapper, ShortTable } from './style'
|
||||||
|
|
||||||
|
const { memo } = require('react')
|
||||||
|
|
||||||
|
const Table = memo(
|
||||||
|
({ playableFileList, viewedFileList, selectedSeason, seasonAmount, hash }) => {
|
||||||
|
const preloadBuffer = fileId => fetch(`${streamHost()}?link=${hash}&index=${fileId}&preload`)
|
||||||
|
const getFileLink = (path, id) =>
|
||||||
|
`${streamHost()}/${encodeURIComponent(path.split('\\').pop().split('/').pop())}?link=${hash}&index=${id}&play`
|
||||||
|
const fileHasEpisodeText = !!playableFileList?.find(({ path }) => ptt.parse(path).episode)
|
||||||
|
const fileHasSeasonText = !!playableFileList?.find(({ path }) => ptt.parse(path).season)
|
||||||
|
const fileHasResolutionText = !!playableFileList?.find(({ path }) => ptt.parse(path).resolution)
|
||||||
|
|
||||||
|
return !playableFileList?.length ? (
|
||||||
|
'No playable files in this torrent'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TableStyle>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '0' }}>viewed</th>
|
||||||
|
<th>name</th>
|
||||||
|
{fileHasSeasonText && seasonAmount?.length === 1 && <th style={{ width: '0' }}>season</th>}
|
||||||
|
{fileHasEpisodeText && <th style={{ width: '0' }}>episode</th>}
|
||||||
|
{fileHasResolutionText && <th style={{ width: '0' }}>resolution</th>}
|
||||||
|
<th style={{ width: '100px' }}>size</th>
|
||||||
|
<th style={{ width: '400px' }}>actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{playableFileList.map(({ id, path, length }) => {
|
||||||
|
const { title, resolution, episode, season } = ptt.parse(path)
|
||||||
|
const isViewed = viewedFileList?.includes(id)
|
||||||
|
const link = getFileLink(path, id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
(season === selectedSeason || !seasonAmount?.length) && (
|
||||||
|
<tr key={id} className={isViewed ? 'viewed-file-row' : null}>
|
||||||
|
<td data-label='viewed' className={isViewed ? 'viewed-file-indicator' : null} />
|
||||||
|
<td data-label='name'>{title}</td>
|
||||||
|
{fileHasSeasonText && seasonAmount?.length === 1 && <td data-label='season'>{season}</td>}
|
||||||
|
{fileHasEpisodeText && <td data-label='episode'>{episode}</td>}
|
||||||
|
{fileHasResolutionText && <td data-label='resolution'>{resolution}</td>}
|
||||||
|
<td data-label='size'>{humanizeSize(length)}</td>
|
||||||
|
<td className='button-cell'>
|
||||||
|
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
|
||||||
|
Preload
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
|
||||||
|
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
|
||||||
|
Open link
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<CopyToClipboard text={link}>
|
||||||
|
<Button variant='outlined' color='primary' size='small'>
|
||||||
|
Copy link
|
||||||
|
</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</TableStyle>
|
||||||
|
<ShortTableWrapper>
|
||||||
|
{playableFileList.map(({ id, path, length }) => {
|
||||||
|
const { title, resolution, episode, season } = ptt.parse(path)
|
||||||
|
const isViewed = viewedFileList?.includes(id)
|
||||||
|
const link = getFileLink(path, id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
(season === selectedSeason || !seasonAmount?.length) && (
|
||||||
|
<ShortTable key={id} isViewed={isViewed}>
|
||||||
|
<div className='short-table-name'>{title}</div>
|
||||||
|
<div className='short-table-data'>
|
||||||
|
<div className='short-table-field'>
|
||||||
|
<div className='short-table-field-name'>viewed</div>
|
||||||
|
<div className='short-table-field-value'>
|
||||||
|
<div className='short-table-viewed-indicator' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fileHasSeasonText && seasonAmount?.length === 1 && (
|
||||||
|
<div className='short-table-field'>
|
||||||
|
<div className='short-table-field-name'>season</div>
|
||||||
|
<div className='short-table-field-value'>{season}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{fileHasEpisodeText && (
|
||||||
|
<div className='short-table-field'>
|
||||||
|
<div className='short-table-field-name'>epoisode</div>
|
||||||
|
<div className='short-table-field-value'>{episode}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{fileHasResolutionText && (
|
||||||
|
<div className='short-table-field'>
|
||||||
|
<div className='short-table-field-name'>resolution</div>
|
||||||
|
<div className='short-table-field-value'>{resolution}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='short-table-field'>
|
||||||
|
<div className='short-table-field-name'>size</div>
|
||||||
|
<div className='short-table-field-value'>{humanizeSize(length)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='short-table-buttons'>
|
||||||
|
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
|
||||||
|
Preload
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
|
||||||
|
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
|
||||||
|
Open link
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<CopyToClipboard text={link}>
|
||||||
|
<Button variant='outlined' color='primary' size='small'>
|
||||||
|
Copy link
|
||||||
|
</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</ShortTable>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ShortTableWrapper>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(prev, next) => isEqual(prev, next),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Table
|
||||||
171
web/src/components/DialogTorrentDetailsContent/Table/style.js
Normal file
171
web/src/components/DialogTorrentDetailsContent/Table/style.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
|
const viewedIndicator = css`
|
||||||
|
:before {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #15d5af;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const TableStyle = styled.table`
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 25px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
background: #009879;
|
||||||
|
color: #fff;
|
||||||
|
text-align: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
|
||||||
|
:last-of-type {
|
||||||
|
border-bottom: 2px solid #009879;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.viewed-file-row {
|
||||||
|
background: #f3f3f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
&.viewed-file-indicator {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${viewedIndicator}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button-cell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 970px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ShortTableWrapper = styled.div`
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (max-width: 970px) {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
gap: 15px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const ShortTable = styled.div`
|
||||||
|
${({ isViewed }) => css`
|
||||||
|
width: 100%;
|
||||||
|
grid-template-rows: repeat(3, max-content);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.short-table {
|
||||||
|
&-name {
|
||||||
|
background: ${isViewed ? '#bdbdbd' : '#009879'};
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-data {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: max-content;
|
||||||
|
}
|
||||||
|
&-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 30px 1fr;
|
||||||
|
background: black;
|
||||||
|
:not(:last-child) {
|
||||||
|
border-right: 1px solid ${isViewed ? '#bdbdbd' : '#019376'};
|
||||||
|
}
|
||||||
|
|
||||||
|
&-name {
|
||||||
|
background: ${isViewed ? '#c4c4c4' : '#00a383'};
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-value {
|
||||||
|
background: ${isViewed ? '#c9c9c9' : '#03aa89'};
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-viewed-indicator {
|
||||||
|
${isViewed && viewedIndicator}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-buttons {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 2px solid ${isViewed ? '#bdbdbd' : '#009879'};
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
@media (max-width: 410px) {
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`
|
||||||
@@ -1,71 +1,90 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useEffect, useState } from 'react'
|
||||||
import DialogContent from '@material-ui/core/DialogContent'
|
import DialogContent from '@material-ui/core/DialogContent'
|
||||||
import { Stage, Layer } from 'react-konva'
|
import { Stage, Layer } from 'react-konva'
|
||||||
import Measure from 'react-measure'
|
import Measure from 'react-measure'
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
|
||||||
import SingleBlock from './SingleBlock'
|
import SingleBlock from './SingleBlock'
|
||||||
|
import { useCreateCacheMap } from './customHooks'
|
||||||
|
|
||||||
export default function TorrentCache({ cache, cacheMap, isMini }) {
|
const TorrentCache = memo(
|
||||||
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
|
({ cache, isMini }) => {
|
||||||
const [stageSettings, setStageSettings] = useState({
|
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
|
||||||
boxHeight: null,
|
const [stageSettings, setStageSettings] = useState({
|
||||||
strokeWidth: null,
|
boxHeight: null,
|
||||||
marginBetweenBlocks: null,
|
strokeWidth: null,
|
||||||
stageOffset: null,
|
marginBetweenBlocks: null,
|
||||||
})
|
stageOffset: null,
|
||||||
|
|
||||||
const updateStageSettings = (boxHeight, strokeWidth) => {
|
|
||||||
setStageSettings({
|
|
||||||
boxHeight,
|
|
||||||
strokeWidth,
|
|
||||||
marginBetweenBlocks: strokeWidth,
|
|
||||||
stageOffset: strokeWidth * 2,
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
const cacheMap = useCreateCacheMap(cache)
|
||||||
// initializing stageSettings
|
|
||||||
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
|
|
||||||
updateStageSettings(12, 2)
|
|
||||||
}, [isMini, dimensions.width])
|
|
||||||
|
|
||||||
const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings
|
const updateStageSettings = (boxHeight, strokeWidth) => {
|
||||||
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
|
setStageSettings({
|
||||||
const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks
|
boxHeight,
|
||||||
const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin)
|
strokeWidth,
|
||||||
const amountOfBlocksToRenderInShortView =
|
marginBetweenBlocks: strokeWidth,
|
||||||
preloadPiecesAmount === piecesInOneRow
|
stageOffset: strokeWidth * 2,
|
||||||
? preloadPiecesAmount - 1
|
})
|
||||||
: preloadPiecesAmount + piecesInOneRow - (preloadPiecesAmount % piecesInOneRow) - 1
|
}
|
||||||
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
|
|
||||||
let activeId = null
|
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
// initializing stageSettings
|
||||||
{({ measureRef }) => (
|
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
|
||||||
<div ref={measureRef}>
|
updateStageSettings(12, 2)
|
||||||
<DialogContent style={{ padding: 0 }}>
|
}, [isMini, dimensions.width])
|
||||||
<Stage
|
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
offset={{ x: -stageOffset, y: -stageOffset }}
|
|
||||||
width={stageOffset + blockSizeWithMargin * piecesInOneRow || 0}
|
|
||||||
height={stageOffset + blockSizeWithMargin * amountOfRows || 0}
|
|
||||||
>
|
|
||||||
<Layer>
|
|
||||||
{cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
|
|
||||||
const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow)
|
|
||||||
|
|
||||||
// -------- related only for short view -------
|
const { boxHeight, strokeWidth, marginBetweenBlocks, stageOffset } = stageSettings
|
||||||
if (isActive) activeId = id
|
const preloadPiecesAmount = Math.round(cache.Capacity / cache.PiecesLength - 1)
|
||||||
const shouldBeRendered =
|
const blockSizeWithMargin = boxHeight + strokeWidth + marginBetweenBlocks
|
||||||
isActive || (id - activeId <= amountOfBlocksToRenderInShortView && id - activeId >= 0)
|
const piecesInOneRow = Math.floor((dimensions.width * 0.9) / blockSizeWithMargin)
|
||||||
// --------------------------------------------
|
const amountOfBlocksToRenderInShortView =
|
||||||
|
preloadPiecesAmount === piecesInOneRow
|
||||||
|
? preloadPiecesAmount - 1
|
||||||
|
: preloadPiecesAmount + piecesInOneRow - (preloadPiecesAmount % piecesInOneRow) - 1
|
||||||
|
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
|
||||||
|
let activeId = null
|
||||||
|
|
||||||
return isMini ? (
|
return (
|
||||||
shouldBeRendered && (
|
<Measure bounds onResize={({ bounds }) => setDimensions(bounds)}>
|
||||||
|
{({ measureRef }) => (
|
||||||
|
<div ref={measureRef}>
|
||||||
|
<DialogContent 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>
|
||||||
|
{cacheMap.map(({ id, percentage, isComplete, inProgress, isActive, isReaderRange }) => {
|
||||||
|
const currentRow = Math.floor((isMini ? id - activeId : id) / piecesInOneRow)
|
||||||
|
|
||||||
|
// -------- related only for short view -------
|
||||||
|
if (isActive) activeId = id
|
||||||
|
const shouldBeRendered =
|
||||||
|
isActive || (id - activeId <= amountOfBlocksToRenderInShortView && id - activeId >= 0)
|
||||||
|
// --------------------------------------------
|
||||||
|
|
||||||
|
return isMini ? (
|
||||||
|
shouldBeRendered && (
|
||||||
|
<SingleBlock
|
||||||
|
key={id}
|
||||||
|
x={((id - activeId) % piecesInOneRow) * blockSizeWithMargin}
|
||||||
|
y={currentRow * blockSizeWithMargin}
|
||||||
|
percentage={percentage}
|
||||||
|
inProgress={inProgress}
|
||||||
|
isComplete={isComplete}
|
||||||
|
isReaderRange={isReaderRange}
|
||||||
|
isActive={isActive}
|
||||||
|
boxHeight={boxHeight}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<SingleBlock
|
<SingleBlock
|
||||||
key={id}
|
key={id}
|
||||||
x={((id - activeId) % piecesInOneRow) * blockSizeWithMargin}
|
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
||||||
y={currentRow * blockSizeWithMargin}
|
y={currentRow * blockSizeWithMargin}
|
||||||
percentage={percentage}
|
percentage={percentage}
|
||||||
inProgress={inProgress}
|
inProgress={inProgress}
|
||||||
@@ -76,26 +95,16 @@ export default function TorrentCache({ cache, cacheMap, isMini }) {
|
|||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
})}
|
||||||
<SingleBlock
|
</Layer>
|
||||||
key={id}
|
</Stage>
|
||||||
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
</DialogContent>
|
||||||
y={currentRow * blockSizeWithMargin}
|
</div>
|
||||||
percentage={percentage}
|
)}
|
||||||
inProgress={inProgress}
|
</Measure>
|
||||||
isComplete={isComplete}
|
)
|
||||||
isReaderRange={isReaderRange}
|
},
|
||||||
isActive={isActive}
|
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
|
||||||
boxHeight={boxHeight}
|
)
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
/>
|
export default TorrentCache
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Layer>
|
|
||||||
</Stage>
|
|
||||||
</DialogContent>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Measure>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { playlistTorrHost, torrentsHost, viewedHost } from 'utils/Hosts'
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
import ptt from 'parse-torrent-title'
|
||||||
|
|
||||||
|
import { SmallLabel, MainSectionButtonGroup } from './style'
|
||||||
|
import { SectionSubName } from '../style'
|
||||||
|
|
||||||
|
const TorrentFunctions = memo(
|
||||||
|
({ hash, viewedFileList, playableFileList, name, title, setViewedFileList }) => {
|
||||||
|
const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1]
|
||||||
|
const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path
|
||||||
|
const isOnlyOnePlayableFile = playableFileList?.length === 1
|
||||||
|
const latestViewedFileData = latestViewedFile && ptt.parse(latestViewedFile)
|
||||||
|
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
|
||||||
|
const removeTorrentViews = () =>
|
||||||
|
axios.post(viewedHost(), { action: 'rem', hash, file_index: -1 }).then(() => setViewedFileList())
|
||||||
|
const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(name || title || 'file')}.m3u?link=${hash}&m3u`
|
||||||
|
const partialPlaylistLink = `${fullPlaylistLink}&fromlast`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isOnlyOnePlayableFile && !!viewedFileList?.length && (
|
||||||
|
<>
|
||||||
|
<SmallLabel>Download Playlist</SmallLabel>
|
||||||
|
<SectionSubName mb={10}>
|
||||||
|
<strong>Latest file played:</strong> {latestViewedFileData?.title}.
|
||||||
|
{latestViewedFileData?.season && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
Season: {latestViewedFileData?.season}. Episode: {latestViewedFileData?.episode}.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SectionSubName>
|
||||||
|
|
||||||
|
<MainSectionButtonGroup>
|
||||||
|
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
||||||
|
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||||
|
full
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a style={{ textDecoration: 'none' }} href={partialPlaylistLink}>
|
||||||
|
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||||
|
from latest file
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</MainSectionButtonGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SmallLabel mb={10}>Torrent State</SmallLabel>
|
||||||
|
<MainSectionButtonGroup>
|
||||||
|
<Button onClick={() => removeTorrentViews()} variant='contained' color='primary' size='large'>
|
||||||
|
remove views
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => dropTorrent()} variant='contained' color='primary' size='large'>
|
||||||
|
drop torrent
|
||||||
|
</Button>
|
||||||
|
</MainSectionButtonGroup>
|
||||||
|
<SmallLabel mb={10}>Info</SmallLabel>
|
||||||
|
<MainSectionButtonGroup>
|
||||||
|
{(isOnlyOnePlayableFile || !viewedFileList?.length) && (
|
||||||
|
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
||||||
|
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
||||||
|
download playlist
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<CopyToClipboard text={hash}>
|
||||||
|
<Button variant='contained' color='primary' size='large'>
|
||||||
|
copy hash
|
||||||
|
</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</MainSectionButtonGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
() => true,
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TorrentFunctions
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
|
export const MainSectionButtonGroup = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
:not(:last-child) {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1580px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SmallLabel = styled.div`
|
||||||
|
${({ mb }) => css`
|
||||||
|
${mb && `margin-bottom: ${mb}px`};
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
font-size: 18px;
|
||||||
|
${mb && `margin-bottom: ${mb / 1.5}px`};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`
|
||||||
68
web/src/components/DialogTorrentDetailsContent/helpers.js
Normal file
68
web/src/components/DialogTorrentDetailsContent/helpers.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const getExt = filename => {
|
||||||
|
const ext = filename.split('.').pop()
|
||||||
|
if (ext === filename) return ''
|
||||||
|
return ext.toLowerCase()
|
||||||
|
}
|
||||||
|
const playableExtList = [
|
||||||
|
// video
|
||||||
|
'3g2',
|
||||||
|
'3gp',
|
||||||
|
'aaf',
|
||||||
|
'asf',
|
||||||
|
'avchd',
|
||||||
|
'avi',
|
||||||
|
'drc',
|
||||||
|
'flv',
|
||||||
|
'iso',
|
||||||
|
'm2v',
|
||||||
|
'm2ts',
|
||||||
|
'm4p',
|
||||||
|
'm4v',
|
||||||
|
'mkv',
|
||||||
|
'mng',
|
||||||
|
'mov',
|
||||||
|
'mp2',
|
||||||
|
'mp4',
|
||||||
|
'mpe',
|
||||||
|
'mpeg',
|
||||||
|
'mpg',
|
||||||
|
'mpv',
|
||||||
|
'mxf',
|
||||||
|
'nsv',
|
||||||
|
'ogg',
|
||||||
|
'ogv',
|
||||||
|
'ts',
|
||||||
|
'qt',
|
||||||
|
'rm',
|
||||||
|
'rmvb',
|
||||||
|
'roq',
|
||||||
|
'svi',
|
||||||
|
'vob',
|
||||||
|
'webm',
|
||||||
|
'wmv',
|
||||||
|
'yuv',
|
||||||
|
// audio
|
||||||
|
'aac',
|
||||||
|
'aiff',
|
||||||
|
'ape',
|
||||||
|
'au',
|
||||||
|
'flac',
|
||||||
|
'gsm',
|
||||||
|
'it',
|
||||||
|
'm3u',
|
||||||
|
'm4a',
|
||||||
|
'mid',
|
||||||
|
'mod',
|
||||||
|
'mp3',
|
||||||
|
'mpa',
|
||||||
|
'pls',
|
||||||
|
'ra',
|
||||||
|
's3m',
|
||||||
|
'sid',
|
||||||
|
'wav',
|
||||||
|
'wma',
|
||||||
|
'xm',
|
||||||
|
]
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const isFilePlayable = fileName => playableExtList.includes(getExt(fileName))
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
import { NoImageIcon } from 'icons'
|
import { NoImageIcon } from 'icons'
|
||||||
import { humanizeSize } from 'utils/Utils'
|
import { humanizeSize } from 'utils/Utils'
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Button } from '@material-ui/core'
|
import { Button, ButtonGroup } from '@material-ui/core'
|
||||||
import ptt from 'parse-torrent-title'
|
import ptt from 'parse-torrent-title'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { playlistTorrHost, streamHost, torrentsHost, viewedHost } from 'utils/Hosts'
|
import { viewedHost } from 'utils/Hosts'
|
||||||
import { GETTING_INFO, IN_DB } from 'torrentStates'
|
import { GETTING_INFO, IN_DB } from 'torrentStates'
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
|
|
||||||
import { useUpdateCache, useCreateCacheMap, useGetSettings } from './customHooks'
|
import { useUpdateCache, useGetSettings } from './customHooks'
|
||||||
import DialogHeader from './DialogHeader'
|
import DialogHeader from './DialogHeader'
|
||||||
import TorrentCache from './TorrentCache'
|
import TorrentCache from './TorrentCache'
|
||||||
|
import Table from './Table'
|
||||||
import {
|
import {
|
||||||
DetailedViewWidgetSection,
|
DetailedViewWidgetSection,
|
||||||
DetailedViewCacheSection,
|
DetailedViewCacheSection,
|
||||||
DialogContentGrid,
|
DialogContentGrid,
|
||||||
MainSection,
|
MainSection,
|
||||||
MainSectionButtonGroup,
|
|
||||||
Poster,
|
Poster,
|
||||||
SectionTitle,
|
SectionTitle,
|
||||||
SectionSubName,
|
SectionSubName,
|
||||||
@@ -27,8 +26,6 @@ import {
|
|||||||
CacheSection,
|
CacheSection,
|
||||||
TorrentFilesSection,
|
TorrentFilesSection,
|
||||||
Divider,
|
Divider,
|
||||||
SmallLabel,
|
|
||||||
Table,
|
|
||||||
} from './style'
|
} from './style'
|
||||||
import {
|
import {
|
||||||
DownlodSpeedWidget,
|
DownlodSpeedWidget,
|
||||||
@@ -39,6 +36,8 @@ import {
|
|||||||
PiecesLengthWidget,
|
PiecesLengthWidget,
|
||||||
StatusWidget,
|
StatusWidget,
|
||||||
} from './widgets'
|
} from './widgets'
|
||||||
|
import TorrentFunctions from './TorrentFunctions'
|
||||||
|
import { isFilePlayable } from './helpers'
|
||||||
|
|
||||||
const shortenText = (text, count) => text.slice(0, count) + (text.length > count ? '...' : '')
|
const shortenText = (text, count) => text.slice(0, count) + (text.length > count ? '...' : '')
|
||||||
|
|
||||||
@@ -47,11 +46,8 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
const [isDetailedCacheView, setIsDetailedCacheView] = useState(false)
|
const [isDetailedCacheView, setIsDetailedCacheView] = useState(false)
|
||||||
const [viewedFileList, setViewedFileList] = useState()
|
const [viewedFileList, setViewedFileList] = useState()
|
||||||
const [playableFileList, setPlayableFileList] = useState()
|
const [playableFileList, setPlayableFileList] = useState()
|
||||||
|
const [seasonAmount, setSeasonAmount] = useState(null)
|
||||||
const isOnlyOnePlayableFile = playableFileList?.length === 1
|
const [selectedSeason, setSelectedSeason] = useState()
|
||||||
const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1]
|
|
||||||
const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path
|
|
||||||
const latestViewedFileData = latestViewedFile && ptt.parse(latestViewedFile)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
poster,
|
poster,
|
||||||
@@ -67,26 +63,26 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
} = torrent
|
} = torrent
|
||||||
|
|
||||||
const cache = useUpdateCache(hash)
|
const cache = useUpdateCache(hash)
|
||||||
const cacheMap = useCreateCacheMap(cache)
|
|
||||||
const settings = useGetSettings(cache)
|
const settings = useGetSettings(cache)
|
||||||
|
|
||||||
const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash })
|
|
||||||
const removeTorrentViews = () =>
|
|
||||||
axios.post(viewedHost(), { action: 'rem', hash, file_index: -1 }).then(() => setViewedFileList())
|
|
||||||
const preloadBuffer = fileId => fetch(`${streamHost()}?link=${hash}&index=${fileId}&preload`)
|
|
||||||
const getFileLink = (path, id) =>
|
|
||||||
`${streamHost()}/${encodeURIComponent(path.split('\\').pop().split('/').pop())}?link=${hash}&index=${id}&play`
|
|
||||||
const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(name || title || 'file')}.m3u?link=${hash}&m3u`
|
|
||||||
const partialPlaylistLink = `${fullPlaylistLink}&fromlast`
|
|
||||||
|
|
||||||
const fileHasEpisodeText = !!playableFileList?.find(({ path }) => ptt.parse(path).episode)
|
|
||||||
const fileHasSeasonText = !!playableFileList?.find(({ path }) => ptt.parse(path).season)
|
|
||||||
const fileHasResolutionText = !!playableFileList?.find(({ path }) => ptt.parse(path).resolution)
|
|
||||||
|
|
||||||
const { Capacity, PiecesCount, PiecesLength, Filled } = cache
|
const { Capacity, PiecesCount, PiecesLength, Filled } = cache
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPlayableFileList(torrentFileList?.filter(file => playableExtList.includes(getExt(file.path))))
|
if (playableFileList && seasonAmount === null) {
|
||||||
|
const seasons = []
|
||||||
|
playableFileList.forEach(({ path }) => {
|
||||||
|
const currentSeason = ptt.parse(path).season
|
||||||
|
if (currentSeason) {
|
||||||
|
!seasons.includes(currentSeason) && seasons.push(currentSeason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
seasons.length && setSelectedSeason(seasons[0])
|
||||||
|
setSeasonAmount(seasons.sort((a, b) => a - b))
|
||||||
|
}
|
||||||
|
}, [playableFileList, seasonAmount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPlayableFileList(torrentFileList?.filter(({ path }) => isFilePlayable(path)))
|
||||||
}, [torrentFileList])
|
}, [torrentFileList])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -139,7 +135,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
|
|
||||||
<DetailedViewCacheSection>
|
<DetailedViewCacheSection>
|
||||||
<SectionTitle mb={20}>Cache</SectionTitle>
|
<SectionTitle mb={20}>Cache</SectionTitle>
|
||||||
<TorrentCache cache={cache} cacheMap={cacheMap} />
|
<TorrentCache cache={cache} />
|
||||||
</DetailedViewCacheSection>
|
</DetailedViewCacheSection>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -166,62 +162,14 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{!isOnlyOnePlayableFile && !!viewedFileList?.length && (
|
<TorrentFunctions
|
||||||
<>
|
hash={hash}
|
||||||
<SmallLabel>Download Playlist</SmallLabel>
|
viewedFileList={viewedFileList}
|
||||||
<SectionSubName mb={10}>
|
playableFileList={playableFileList}
|
||||||
<strong>Latest file played:</strong> {latestViewedFileData.title}.
|
name={name}
|
||||||
{latestViewedFileData.season && (
|
title={title}
|
||||||
<>
|
setViewedFileList={setViewedFileList}
|
||||||
{' '}
|
/>
|
||||||
Season: {latestViewedFileData.season}. Episode: {latestViewedFileData.episode}.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SectionSubName>
|
|
||||||
|
|
||||||
<MainSectionButtonGroup>
|
|
||||||
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
|
||||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
|
||||||
full
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a style={{ textDecoration: 'none' }} href={partialPlaylistLink}>
|
|
||||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
|
||||||
from latest file
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</MainSectionButtonGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SmallLabel mb={10}>Torrent State</SmallLabel>
|
|
||||||
|
|
||||||
<MainSectionButtonGroup>
|
|
||||||
<Button onClick={() => removeTorrentViews()} variant='contained' color='primary' size='large'>
|
|
||||||
remove views
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => dropTorrent()} variant='contained' color='primary' size='large'>
|
|
||||||
drop torrent
|
|
||||||
</Button>
|
|
||||||
</MainSectionButtonGroup>
|
|
||||||
|
|
||||||
<SmallLabel mb={10}>Info</SmallLabel>
|
|
||||||
|
|
||||||
<MainSectionButtonGroup>
|
|
||||||
{(isOnlyOnePlayableFile || !viewedFileList?.length) && (
|
|
||||||
<a style={{ textDecoration: 'none' }} href={fullPlaylistLink}>
|
|
||||||
<Button style={{ width: '100%' }} variant='contained' color='primary' size='large'>
|
|
||||||
download playlist
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<CopyToClipboard text={hash}>
|
|
||||||
<Button variant='contained' color='primary' size='large'>
|
|
||||||
copy hash
|
|
||||||
</Button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</MainSectionButtonGroup>
|
|
||||||
</div>
|
</div>
|
||||||
</MainSection>
|
</MainSection>
|
||||||
|
|
||||||
@@ -238,7 +186,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
/>
|
/>
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
|
||||||
<TorrentCache isMini cache={cache} cacheMap={cacheMap} />
|
<TorrentCache isMini cache={cache} />
|
||||||
<Button
|
<Button
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
variant='contained'
|
variant='contained'
|
||||||
@@ -250,134 +198,37 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</CacheSection>
|
</CacheSection>
|
||||||
|
|
||||||
{/* <TorrentFilesSection>
|
<TorrentFilesSection>
|
||||||
<SectionTitle mb={20}>Torrent Content</SectionTitle>
|
<SectionTitle mb={20}>Torrent Content</SectionTitle>
|
||||||
|
|
||||||
{!playableFileList?.length ? (
|
{seasonAmount?.length > 1 && (
|
||||||
'No playable files in this torrent'
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<Table>
|
<SectionSubName>Select Season</SectionSubName>
|
||||||
<thead>
|
<ButtonGroup style={{ marginBottom: '10px' }} color='primary'>
|
||||||
<tr>
|
{seasonAmount.map(season => (
|
||||||
<th style={{ width: '0' }}>viewed</th>
|
<Button
|
||||||
<th>name</th>
|
key={season}
|
||||||
{fileHasSeasonText && <th style={{ width: '0' }}>season</th>}
|
variant={selectedSeason === season ? 'contained' : 'outlined'}
|
||||||
{fileHasEpisodeText && <th style={{ width: '0' }}>episode</th>}
|
onClick={() => setSelectedSeason(season)}
|
||||||
{fileHasResolutionText && <th style={{ width: '0' }}>resolution</th>}
|
>
|
||||||
<th style={{ width: '100px' }}>size</th>
|
{season}
|
||||||
<th style={{ width: '400px' }}>actions</th>
|
</Button>
|
||||||
</tr>
|
))}
|
||||||
</thead>
|
</ButtonGroup>
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{playableFileList.map(({ id, path, length }) => {
|
|
||||||
const { title, resolution, episode, season } = ptt.parse(path)
|
|
||||||
const isViewed = viewedFileList?.includes(id)
|
|
||||||
const link = getFileLink(path, id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={id} className={isViewed ? 'viewed-file-row' : null}>
|
|
||||||
<td className={isViewed ? 'viewed-file-indicator' : null} />
|
|
||||||
<td>{title}</td>
|
|
||||||
{fileHasSeasonText && <td>{season}</td>}
|
|
||||||
{fileHasEpisodeText && <td>{episode}</td>}
|
|
||||||
{fileHasResolutionText && <td>{resolution}</td>}
|
|
||||||
<td>{humanizeSize(length)}</td>
|
|
||||||
<td className='button-cell'>
|
|
||||||
<Button onClick={() => preloadBuffer(id)} variant='outlined' color='primary' size='small'>
|
|
||||||
Preload
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
|
|
||||||
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
|
|
||||||
Open link
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<CopyToClipboard text={link}>
|
|
||||||
<Button variant='outlined' color='primary' size='small'>
|
|
||||||
Copy link
|
|
||||||
</Button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TorrentFilesSection> */}
|
|
||||||
|
<Table
|
||||||
|
hash={hash}
|
||||||
|
playableFileList={playableFileList}
|
||||||
|
viewedFileList={viewedFileList}
|
||||||
|
selectedSeason={selectedSeason}
|
||||||
|
seasonAmount={seasonAmount}
|
||||||
|
/>
|
||||||
|
</TorrentFilesSection>
|
||||||
</DialogContentGrid>
|
</DialogContentGrid>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExt(filename) {
|
|
||||||
const ext = filename.split('.').pop()
|
|
||||||
if (ext === filename) return ''
|
|
||||||
return ext.toLowerCase()
|
|
||||||
}
|
|
||||||
const playableExtList = [
|
|
||||||
// video
|
|
||||||
'3g2',
|
|
||||||
'3gp',
|
|
||||||
'aaf',
|
|
||||||
'asf',
|
|
||||||
'avchd',
|
|
||||||
'avi',
|
|
||||||
'drc',
|
|
||||||
'flv',
|
|
||||||
'iso',
|
|
||||||
'm2v',
|
|
||||||
'm2ts',
|
|
||||||
'm4p',
|
|
||||||
'm4v',
|
|
||||||
'mkv',
|
|
||||||
'mng',
|
|
||||||
'mov',
|
|
||||||
'mp2',
|
|
||||||
'mp4',
|
|
||||||
'mpe',
|
|
||||||
'mpeg',
|
|
||||||
'mpg',
|
|
||||||
'mpv',
|
|
||||||
'mxf',
|
|
||||||
'nsv',
|
|
||||||
'ogg',
|
|
||||||
'ogv',
|
|
||||||
'ts',
|
|
||||||
'qt',
|
|
||||||
'rm',
|
|
||||||
'rmvb',
|
|
||||||
'roq',
|
|
||||||
'svi',
|
|
||||||
'vob',
|
|
||||||
'webm',
|
|
||||||
'wmv',
|
|
||||||
'yuv',
|
|
||||||
// audio
|
|
||||||
'aac',
|
|
||||||
'aiff',
|
|
||||||
'ape',
|
|
||||||
'au',
|
|
||||||
'flac',
|
|
||||||
'gsm',
|
|
||||||
'it',
|
|
||||||
'm3u',
|
|
||||||
'm4a',
|
|
||||||
'mid',
|
|
||||||
'mod',
|
|
||||||
'mp3',
|
|
||||||
'mpa',
|
|
||||||
'pls',
|
|
||||||
'ra',
|
|
||||||
's3m',
|
|
||||||
'sid',
|
|
||||||
'wav',
|
|
||||||
'wma',
|
|
||||||
'xm',
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -70,23 +70,9 @@ export const MainSection = styled.section`
|
|||||||
@media (max-width: 840px) {
|
@media (max-width: 840px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
|
||||||
export const MainSectionButtonGroup = styled.div`
|
@media (max-width: 800px) {
|
||||||
display: grid;
|
padding: 20px;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
:not(:last-child) {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1045px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 880px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -97,12 +83,20 @@ export const CacheSection = styled.section`
|
|||||||
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: #88cdaa;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const TorrentFilesSection = styled.section`
|
export const TorrentFilesSection = styled.section`
|
||||||
grid-area: file-list;
|
grid-area: file-list;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
|
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const SectionSubName = styled.div`
|
export const SectionSubName = styled.div`
|
||||||
@@ -159,7 +153,13 @@ export const WidgetWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
: css`
|
: css`
|
||||||
@media (max-width: 840px) {
|
@media (max-width: 800px) {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(max-content, 185px));
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 390px) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
@@ -252,77 +252,6 @@ export const Divider = styled.div`
|
|||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const SmallLabel = styled.div`
|
|
||||||
${({ mb }) => css`
|
|
||||||
${mb && `margin-bottom: ${mb}px`};
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
font-size: 18px;
|
|
||||||
${mb && `margin-bottom: ${mb / 1.5}px`};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const Table = styled.table`
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 25px 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
|
||||||
|
|
||||||
thead tr {
|
|
||||||
background: #009879;
|
|
||||||
color: #fff;
|
|
||||||
text-align: left;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 12px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
|
|
||||||
:last-of-type {
|
|
||||||
border-bottom: 2px solid #009879;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.viewed-file-row {
|
|
||||||
background: #f3f3f3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
&.viewed-file-indicator {
|
|
||||||
position: relative;
|
|
||||||
:before {
|
|
||||||
content: '';
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background: #15d5af;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.button-cell {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const DetailedViewWidgetSection = styled.section`
|
export const DetailedViewWidgetSection = styled.section`
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
|
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
|
||||||
|
|||||||
@@ -8065,7 +8065,7 @@ lodash.uniq@^4.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||||
|
|
||||||
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@^4.7.0:
|
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5, lodash@^4.7.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|||||||
Reference in New Issue
Block a user