This commit is contained in:
Daniel Shleifman
2021-06-01 18:55:31 +03:00
parent 9109f0f694
commit 950b84e34e
10 changed files with 660 additions and 371 deletions

View File

@@ -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",

View 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

View 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;
}
}
}
`}
`

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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`};
}
`}
`

View 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))

View File

@@ -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',
]

View File

@@ -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);

View File

@@ -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==