mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 21:46:11 +05:00
refactor
This commit is contained in:
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 { Stage, Layer } from 'react-konva'
|
||||
import Measure from 'react-measure'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import SingleBlock from './SingleBlock'
|
||||
import { useCreateCacheMap } from './customHooks'
|
||||
|
||||
export default function TorrentCache({ cache, cacheMap, isMini }) {
|
||||
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
|
||||
const [stageSettings, setStageSettings] = useState({
|
||||
boxHeight: null,
|
||||
strokeWidth: null,
|
||||
marginBetweenBlocks: null,
|
||||
stageOffset: null,
|
||||
})
|
||||
|
||||
const updateStageSettings = (boxHeight, strokeWidth) => {
|
||||
setStageSettings({
|
||||
boxHeight,
|
||||
strokeWidth,
|
||||
marginBetweenBlocks: strokeWidth,
|
||||
stageOffset: strokeWidth * 2,
|
||||
const TorrentCache = memo(
|
||||
({ cache, isMini }) => {
|
||||
const [dimensions, setDimensions] = useState({ width: -1, height: -1 })
|
||||
const [stageSettings, setStageSettings] = useState({
|
||||
boxHeight: null,
|
||||
strokeWidth: null,
|
||||
marginBetweenBlocks: null,
|
||||
stageOffset: null,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// initializing stageSettings
|
||||
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
|
||||
updateStageSettings(12, 2)
|
||||
}, [isMini, dimensions.width])
|
||||
const cacheMap = useCreateCacheMap(cache)
|
||||
|
||||
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
|
||||
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
|
||||
let activeId = null
|
||||
const updateStageSettings = (boxHeight, strokeWidth) => {
|
||||
setStageSettings({
|
||||
boxHeight,
|
||||
strokeWidth,
|
||||
marginBetweenBlocks: strokeWidth,
|
||||
stageOffset: strokeWidth * 2,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<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)
|
||||
useEffect(() => {
|
||||
// initializing stageSettings
|
||||
if (isMini) return dimensions.width < 500 ? updateStageSettings(20, 3) : updateStageSettings(24, 4)
|
||||
updateStageSettings(12, 2)
|
||||
}, [isMini, dimensions.width])
|
||||
|
||||
// -------- related only for short view -------
|
||||
if (isActive) activeId = id
|
||||
const shouldBeRendered =
|
||||
isActive || (id - activeId <= amountOfBlocksToRenderInShortView && id - activeId >= 0)
|
||||
// --------------------------------------------
|
||||
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
|
||||
const amountOfRows = Math.ceil((isMini ? amountOfBlocksToRenderInShortView : cacheMap.length) / piecesInOneRow)
|
||||
let activeId = null
|
||||
|
||||
return isMini ? (
|
||||
shouldBeRendered && (
|
||||
return (
|
||||
<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
|
||||
key={id}
|
||||
x={((id - activeId) % piecesInOneRow) * blockSizeWithMargin}
|
||||
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
@@ -76,26 +95,16 @@ export default function TorrentCache({ cache, cacheMap, isMini }) {
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<SingleBlock
|
||||
key={id}
|
||||
x={(id % piecesInOneRow) * blockSizeWithMargin}
|
||||
y={currentRow * blockSizeWithMargin}
|
||||
percentage={percentage}
|
||||
inProgress={inProgress}
|
||||
isComplete={isComplete}
|
||||
isReaderRange={isReaderRange}
|
||||
isActive={isActive}
|
||||
boxHeight={boxHeight}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</DialogContent>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
)
|
||||
},
|
||||
(prev, next) => isEqual(prev.cache.Pieces, next.cache.Pieces) && isEqual(prev.cache.Readers, next.cache.Readers),
|
||||
)
|
||||
|
||||
export default TorrentCache
|
||||
|
||||
@@ -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 { humanizeSize } from 'utils/Utils'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
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 axios from 'axios'
|
||||
import { playlistTorrHost, streamHost, torrentsHost, viewedHost } from 'utils/Hosts'
|
||||
import { viewedHost } from 'utils/Hosts'
|
||||
import { GETTING_INFO, IN_DB } from 'torrentStates'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
|
||||
import { useUpdateCache, useCreateCacheMap, useGetSettings } from './customHooks'
|
||||
import { useUpdateCache, useGetSettings } from './customHooks'
|
||||
import DialogHeader from './DialogHeader'
|
||||
import TorrentCache from './TorrentCache'
|
||||
import Table from './Table'
|
||||
import {
|
||||
DetailedViewWidgetSection,
|
||||
DetailedViewCacheSection,
|
||||
DialogContentGrid,
|
||||
MainSection,
|
||||
MainSectionButtonGroup,
|
||||
Poster,
|
||||
SectionTitle,
|
||||
SectionSubName,
|
||||
@@ -27,8 +26,6 @@ import {
|
||||
CacheSection,
|
||||
TorrentFilesSection,
|
||||
Divider,
|
||||
SmallLabel,
|
||||
Table,
|
||||
} from './style'
|
||||
import {
|
||||
DownlodSpeedWidget,
|
||||
@@ -39,6 +36,8 @@ import {
|
||||
PiecesLengthWidget,
|
||||
StatusWidget,
|
||||
} from './widgets'
|
||||
import TorrentFunctions from './TorrentFunctions'
|
||||
import { isFilePlayable } from './helpers'
|
||||
|
||||
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 [viewedFileList, setViewedFileList] = useState()
|
||||
const [playableFileList, setPlayableFileList] = useState()
|
||||
|
||||
const isOnlyOnePlayableFile = playableFileList?.length === 1
|
||||
const latestViewedFileId = viewedFileList?.[viewedFileList?.length - 1]
|
||||
const latestViewedFile = playableFileList?.find(({ id }) => id === latestViewedFileId)?.path
|
||||
const latestViewedFileData = latestViewedFile && ptt.parse(latestViewedFile)
|
||||
const [seasonAmount, setSeasonAmount] = useState(null)
|
||||
const [selectedSeason, setSelectedSeason] = useState()
|
||||
|
||||
const {
|
||||
poster,
|
||||
@@ -67,26 +63,26 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
} = torrent
|
||||
|
||||
const cache = useUpdateCache(hash)
|
||||
const cacheMap = useCreateCacheMap(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
|
||||
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,7 +135,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
|
||||
<DetailedViewCacheSection>
|
||||
<SectionTitle mb={20}>Cache</SectionTitle>
|
||||
<TorrentCache cache={cache} cacheMap={cacheMap} />
|
||||
<TorrentCache cache={cache} />
|
||||
</DetailedViewCacheSection>
|
||||
</>
|
||||
) : (
|
||||
@@ -166,62 +162,14 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
|
||||
<Divider />
|
||||
|
||||
{!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>
|
||||
<TorrentFunctions
|
||||
hash={hash}
|
||||
viewedFileList={viewedFileList}
|
||||
playableFileList={playableFileList}
|
||||
name={name}
|
||||
title={title}
|
||||
setViewedFileList={setViewedFileList}
|
||||
/>
|
||||
</div>
|
||||
</MainSection>
|
||||
|
||||
@@ -238,7 +186,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
/>
|
||||
</SectionHeader>
|
||||
|
||||
<TorrentCache isMini cache={cache} cacheMap={cacheMap} />
|
||||
<TorrentCache isMini cache={cache} />
|
||||
<Button
|
||||
style={{ marginTop: '30px' }}
|
||||
variant='contained'
|
||||
@@ -250,134 +198,37 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
|
||||
</Button>
|
||||
</CacheSection>
|
||||
|
||||
{/* <TorrentFilesSection>
|
||||
<TorrentFilesSection>
|
||||
<SectionTitle mb={20}>Torrent Content</SectionTitle>
|
||||
|
||||
{!playableFileList?.length ? (
|
||||
'No playable files in this torrent'
|
||||
) : (
|
||||
{seasonAmount?.length > 1 && (
|
||||
<>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '0' }}>viewed</th>
|
||||
<th>name</th>
|
||||
{fileHasSeasonText && <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 (
|
||||
<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>
|
||||
<SectionSubName>Select Season</SectionSubName>
|
||||
<ButtonGroup style={{ marginBottom: '10px' }} color='primary'>
|
||||
{seasonAmount.map(season => (
|
||||
<Button
|
||||
key={season}
|
||||
variant={selectedSeason === season ? 'contained' : 'outlined'}
|
||||
onClick={() => setSelectedSeason(season)}
|
||||
>
|
||||
{season}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
)}
|
||||
</TorrentFilesSection> */}
|
||||
|
||||
<Table
|
||||
hash={hash}
|
||||
playableFileList={playableFileList}
|
||||
viewedFileList={viewedFileList}
|
||||
selectedSeason={selectedSeason}
|
||||
seasonAmount={seasonAmount}
|
||||
/>
|
||||
</TorrentFilesSection>
|
||||
</DialogContentGrid>
|
||||
)}
|
||||
</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) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
export const MainSectionButtonGroup = styled.div`
|
||||
display: grid;
|
||||
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;
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -97,12 +83,20 @@ export const CacheSection = styled.section`
|
||||
align-content: start;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
background: #88cdaa;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const TorrentFilesSection = styled.section`
|
||||
grid-area: file-list;
|
||||
padding: 40px;
|
||||
box-shadow: inset 3px 25px 8px -25px rgba(0, 0, 0, 0.5);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const SectionSubName = styled.div`
|
||||
@@ -159,7 +153,13 @@ export const WidgetWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
: 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;
|
||||
}
|
||||
`}
|
||||
@@ -252,77 +252,6 @@ export const Divider = styled.div`
|
||||
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`
|
||||
padding: 40px;
|
||||
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
|
||||
|
||||
Reference in New Issue
Block a user