Merge branch 'addDialog-refactor'

This commit is contained in:
Daniel Shleifman
2021-06-10 23:51:06 +03:00
5 changed files with 321 additions and 210 deletions

View File

@@ -1,65 +1,46 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Dialog from '@material-ui/core/Dialog' import Dialog from '@material-ui/core/Dialog'
import { torrentsHost, torrentUploadHost } from 'utils/Hosts' import { torrentsHost, torrentUploadHost } from 'utils/Hosts'
import axios from 'axios' import axios from 'axios'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { NoImageIcon, AddItemIcon, TorrentIcon } from 'icons'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import { v4 as uuidv4 } from 'uuid'
import useChangeLanguage from 'utils/useChangeLanguage' import useChangeLanguage from 'utils/useChangeLanguage'
import { Cancel as CancelIcon } from '@material-ui/icons'
import { useDropzone } from 'react-dropzone'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery } from '@material-ui/core'
import parseTorrent from 'parse-torrent'
import ptt from 'parse-torrent-title'
import CircularProgress from '@material-ui/core/CircularProgress' import CircularProgress from '@material-ui/core/CircularProgress'
import usePreviousState from 'utils/usePreviousState'
import { checkImageURL, getMoviePosters, chechTorrentSource } from './helpers' import { checkImageURL, getMoviePosters, chechTorrentSource, parseTorrentTitle } from './helpers'
import { import { ButtonWrapper, Content, Header } from './style'
ButtonWrapper, import RightSideComponent from './RightSideComponent'
CancelIconWrapper, import LeftSideComponent from './LeftSideComponent'
ClearPosterButton,
PosterLanguageSwitch,
Content,
Header,
IconWrapper,
RightSide,
Poster,
PosterSuggestions,
PosterSuggestionsItem,
PosterWrapper,
LeftSide,
LeftSideBottomSectionFileSelected,
LeftSideBottomSectionNoFile,
LeftSideTopSection,
TorrentIconWrapper,
RightSideContainer,
} from './style'
export default function AddDialog({ handleClose }) { export default function AddDialog({ handleClose }) {
const { t } = useTranslation() const { t } = useTranslation()
const [torrentSource, setTorrentSource] = useState('') const [torrentSource, setTorrentSource] = useState('')
const [isTorrentSourceActive, setIsTorrentSourceActive] = useState(false)
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [posterUrl, setPosterUrl] = useState('') const [posterUrl, setPosterUrl] = useState('')
const [isPosterUrlCorrect, setIsPosterUrlCorrect] = useState(false) const [isPosterUrlCorrect, setIsPosterUrlCorrect] = useState(false)
const [isTorrentSourceCorrect, setIsTorrentSourceCorrect] = useState(false) const [isTorrentSourceCorrect, setIsTorrentSourceCorrect] = useState(false)
const [posterList, setPosterList] = useState() const [posterList, setPosterList] = useState()
const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(false) const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(false)
const [isUserInteractedWithTitle, setIsUserInteractedWithTitle] = useState(false)
const [currentLang] = useChangeLanguage() const [currentLang] = useChangeLanguage()
const [selectedFile, setSelectedFile] = useState() const [selectedFile, setSelectedFile] = useState()
const [posterSearchLanguage, setPosterSearchLanguage] = useState(currentLang === 'ru' ? 'ru' : 'en') const [posterSearchLanguage, setPosterSearchLanguage] = useState(currentLang === 'ru' ? 'ru' : 'en')
const [isLoadingButton, setIsLoadingButton] = useState(false) const [isLoadingButton, setIsLoadingButton] = useState(false)
const [skipDebounce, setSkipDebounce] = useState(false)
const fullScreen = useMediaQuery('@media (max-width:930px)') const fullScreen = useMediaQuery('@media (max-width:930px)')
const posterSearch = useMemo( const posterSearch = useMemo(
() => () =>
(movieName, language, settings = {}) => { (movieName, language, { shouldRefreshMainPoster = false } = {}) => {
const { shouldRefreshMainPoster = false } = settings if (!movieName) {
setPosterList()
removePoster()
return
}
getMoviePosters(movieName, language).then(urlList => { getMoviePosters(movieName, language).then(urlList => {
if (urlList) { if (urlList) {
setPosterList(urlList) setPosterList(urlList)
@@ -85,65 +66,54 @@ export default function AddDialog({ handleClose }) {
const delayedPosterSearch = useMemo(() => debounce(posterSearch, 700), [posterSearch]) const delayedPosterSearch = useMemo(() => debounce(posterSearch, 700), [posterSearch])
const prevTitleState = usePreviousState(title)
const prevTorrentSourceState = usePreviousState(torrentSource)
useEffect(() => { useEffect(() => {
if (isUserInteractedWithTitle) return // if torrentSource is updated then we are checking that source is valid and getting title from the source
const torrentSourceChanged = torrentSource !== prevTorrentSourceState
parseTorrent.remote(selectedFile || torrentSource, (err, parsedTorrent) => { const isCorrectSource = chechTorrentSource(torrentSource)
if (err) throw err if (!isCorrectSource) return setIsTorrentSourceCorrect(false)
if (!parsedTorrent.name) return
const torrentName = ptt.parse(parsedTorrent.name).title setIsTorrentSourceCorrect(true)
const fileInsideTorrentName = parsedTorrent.files ? ptt.parse(parsedTorrent.files[0].name).title : null
let value = torrentName if (torrentSourceChanged) {
if (fileInsideTorrentName) { parseTorrentTitle(selectedFile || torrentSource, newTitle => {
value = torrentName.length < fileInsideTorrentName.length ? torrentName : fileInsideTorrentName if (!newTitle) return
}
setTitle(value) setSkipDebounce(true)
delayedPosterSearch(value, posterSearchLanguage) setTitle(newTitle)
}) })
}, [selectedFile, delayedPosterSearch, torrentSource, posterSearchLanguage, isUserInteractedWithTitle]) }
}, [prevTorrentSourceState, selectedFile, torrentSource])
useEffect(() => { useEffect(() => {
setIsTorrentSourceCorrect(chechTorrentSource(torrentSource)) // if title exists and title was changed then search poster.
const titleChanged = title !== prevTitleState
if (!titleChanged) return
if (!torrentSource) { if (skipDebounce) {
setPosterUrl('') posterSearch(title, posterSearchLanguage)
setPosterList() setSkipDebounce(false)
setIsPosterUrlCorrect(false) } else {
delayedPosterSearch(title, posterSearchLanguage)
} }
}, [torrentSource]) }, [title, prevTitleState, delayedPosterSearch, posterSearch, posterSearchLanguage, skipDebounce])
const handleCapture = files => {
const [file] = files
if (!file) return
setIsUserInteractedWithPoster(false)
setSelectedFile(file)
setTorrentSource(file.name)
}
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: handleCapture, accept: '.torrent' })
const removePoster = () => { const removePoster = () => {
setIsPosterUrlCorrect(false) setIsPosterUrlCorrect(false)
setPosterUrl('') setPosterUrl('')
} }
const handleTorrentSourceChange = ({ target: { value } }) => setTorrentSource(value) useEffect(() => {
const handleTitleChange = ({ target: { value } }) => { if (!selectedFile && !torrentSource) {
setTitle(value) setTitle('')
delayedPosterSearch(value, posterSearchLanguage)
torrentSource && setIsUserInteractedWithTitle(true)
}
const handlePosterUrlChange = ({ target: { value } }) => {
setPosterUrl(value)
checkImageURL(value).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(!!value)
setPosterList() setPosterList()
removePoster()
setIsUserInteractedWithPoster(false)
} }
}, [selectedFile, torrentSource])
const handleSave = () => { const handleSave = () => {
setIsLoadingButton(true) setIsLoadingButton(true)
@@ -155,10 +125,7 @@ export default function AddDialog({ handleClose }) {
data.append('file', selectedFile) data.append('file', selectedFile)
title && data.append('title', title) title && data.append('title', title)
posterUrl && data.append('poster', posterUrl) posterUrl && data.append('poster', posterUrl)
axios axios.post(torrentUploadHost(), data).finally(handleClose)
.post(torrentUploadHost(), data)
// .then(res => console.log(res))
.finally(handleClose)
} else { } else {
// link save // link save
axios axios
@@ -167,18 +134,6 @@ export default function AddDialog({ handleClose }) {
} }
} }
const clearSelectedFile = () => {
setSelectedFile()
setTorrentSource('')
setIsUserInteractedWithTitle(false)
}
const userChangesPosterUrl = url => {
setPosterUrl(url)
checkImageURL(url).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(true)
}
return ( return (
<Dialog <Dialog
open open
@@ -191,120 +146,32 @@ export default function AddDialog({ handleClose }) {
<Header>{t('AddNewTorrent')}</Header> <Header>{t('AddNewTorrent')}</Header>
<Content> <Content>
<LeftSide> <LeftSideComponent
<LeftSideTopSection active={isTorrentSourceActive}> setIsUserInteractedWithPoster={setIsUserInteractedWithPoster}
<TextField setSelectedFile={setSelectedFile}
onChange={handleTorrentSourceChange} torrentSource={torrentSource}
value={torrentSource} setTorrentSource={setTorrentSource}
margin='dense' selectedFile={selectedFile}
label={t('TorrentSourceLink')}
helperText={t('TorrentSourceOptions')}
type='text'
fullWidth
onFocus={() => setIsTorrentSourceActive(true)}
onBlur={() => setIsTorrentSourceActive(false)}
inputProps={{ autoComplete: 'off' }}
disabled={!!selectedFile}
/>
</LeftSideTopSection>
{selectedFile ? (
<LeftSideBottomSectionFileSelected>
<TorrentIconWrapper>
<TorrentIcon />
<CancelIconWrapper onClick={clearSelectedFile}>
<CancelIcon />
</CancelIconWrapper>
</TorrentIconWrapper>
</LeftSideBottomSectionFileSelected>
) : (
<LeftSideBottomSectionNoFile isDragActive={isDragActive} {...getRootProps()}>
<input {...getInputProps()} />
<div>{t('AppendFile.Or')}</div>
<IconWrapper>
<AddItemIcon color='primary' />
<div>{t('AppendFile.ClickOrDrag')}</div>
</IconWrapper>
</LeftSideBottomSectionNoFile>
)}
</LeftSide>
<RightSide>
<RightSideContainer isHidden={!isTorrentSourceCorrect}>
<TextField
onChange={handleTitleChange}
value={title}
margin='dense'
label={t('Title')}
type='text'
fullWidth
/>
<TextField
onChange={handlePosterUrlChange}
value={posterUrl}
margin='dense'
label={t('AddPosterLinkInput')}
type='url'
fullWidth
/> />
<PosterWrapper> <RightSideComponent
<Poster poster={+isPosterUrlCorrect}> setTitle={setTitle}
{isPosterUrlCorrect ? <img src={posterUrl} alt='poster' /> : <NoImageIcon />} setPosterUrl={setPosterUrl}
</Poster> setIsPosterUrlCorrect={setIsPosterUrlCorrect}
setIsUserInteractedWithPoster={setIsUserInteractedWithPoster}
<PosterSuggestions> setPosterList={setPosterList}
{posterList isTorrentSourceCorrect={isTorrentSourceCorrect}
?.filter(url => url !== posterUrl) title={title}
.slice(0, 12) posterUrl={posterUrl}
.map(url => ( isPosterUrlCorrect={isPosterUrlCorrect}
<PosterSuggestionsItem onClick={() => userChangesPosterUrl(url)} key={uuidv4()}> posterList={posterList}
<img src={url} alt='poster' /> currentLang={currentLang}
</PosterSuggestionsItem> posterSearchLanguage={posterSearchLanguage}
))} setPosterSearchLanguage={setPosterSearchLanguage}
</PosterSuggestions> posterSearch={posterSearch}
removePoster={removePoster}
{currentLang !== 'en' && ( torrentSource={torrentSource}
<PosterLanguageSwitch
onClick={() => {
const newLanguage = posterSearchLanguage === 'en' ? 'ru' : 'en'
setPosterSearchLanguage(newLanguage)
posterSearch(title, newLanguage, { shouldRefreshMainPoster: true })
}}
showbutton={+isPosterUrlCorrect}
color='primary'
variant='contained'
size='small'
>
{posterSearchLanguage === 'en' ? 'EN' : 'RU'}
</PosterLanguageSwitch>
)}
<ClearPosterButton
showbutton={+isPosterUrlCorrect}
onClick={() => {
removePoster()
setIsUserInteractedWithPoster(true)
}}
color='primary'
variant='contained'
size='small'
>
{t('Clear')}
</ClearPosterButton>
</PosterWrapper>
</RightSideContainer>
<RightSideContainer
isError={torrentSource && !isTorrentSourceCorrect}
notificationMessage={
!torrentSource ? t('AddTorrentSourceNotification') : !isTorrentSourceCorrect && t('WrongTorrentSource')
}
isHidden={isTorrentSourceCorrect}
/> />
</RightSide>
</Content> </Content>
<ButtonWrapper> <ButtonWrapper>

View File

@@ -0,0 +1,87 @@
import { useTranslation } from 'react-i18next'
import { useDropzone } from 'react-dropzone'
import { AddItemIcon, TorrentIcon } from 'icons'
import TextField from '@material-ui/core/TextField'
import { Cancel as CancelIcon } from '@material-ui/icons'
import { useState } from 'react'
import {
CancelIconWrapper,
IconWrapper,
LeftSide,
LeftSideBottomSectionFileSelected,
LeftSideBottomSectionNoFile,
LeftSideTopSection,
TorrentIconWrapper,
} from './style'
export default function LeftSideComponent({
setIsUserInteractedWithPoster,
setSelectedFile,
torrentSource,
setTorrentSource,
selectedFile,
}) {
const { t } = useTranslation()
const handleCapture = files => {
const [file] = files
if (!file) return
setIsUserInteractedWithPoster(false)
setSelectedFile(file)
setTorrentSource(file.name)
}
const clearSelectedFile = () => {
setSelectedFile()
setTorrentSource('')
}
const [isTorrentSourceActive, setIsTorrentSourceActive] = useState(false)
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: handleCapture, accept: '.torrent' })
const handleTorrentSourceChange = ({ target: { value } }) => setTorrentSource(value)
return (
<LeftSide>
<LeftSideTopSection active={isTorrentSourceActive}>
<TextField
onChange={handleTorrentSourceChange}
value={torrentSource}
margin='dense'
label={t('TorrentSourceLink')}
helperText={t('TorrentSourceOptions')}
type='text'
fullWidth
onFocus={() => setIsTorrentSourceActive(true)}
onBlur={() => setIsTorrentSourceActive(false)}
inputProps={{ autoComplete: 'off' }}
disabled={!!selectedFile}
/>
</LeftSideTopSection>
{selectedFile ? (
<LeftSideBottomSectionFileSelected>
<TorrentIconWrapper>
<TorrentIcon />
<CancelIconWrapper onClick={clearSelectedFile}>
<CancelIcon />
</CancelIconWrapper>
</TorrentIconWrapper>
</LeftSideBottomSectionFileSelected>
) : (
<LeftSideBottomSectionNoFile isDragActive={isDragActive} {...getRootProps()}>
<input {...getInputProps()} />
<div>{t('AppendFile.Or')}</div>
<IconWrapper>
<AddItemIcon color='primary' />
<div>{t('AppendFile.ClickOrDrag')}</div>
</IconWrapper>
</LeftSideBottomSectionNoFile>
)}
</LeftSide>
)
}

View File

@@ -0,0 +1,120 @@
import { useTranslation } from 'react-i18next'
import { v4 as uuidv4 } from 'uuid'
import { NoImageIcon } from 'icons'
import { TextField } from '@material-ui/core'
import {
ClearPosterButton,
PosterLanguageSwitch,
RightSide,
Poster,
PosterSuggestions,
PosterSuggestionsItem,
PosterWrapper,
RightSideContainer,
} from './style'
import { checkImageURL } from './helpers'
export default function RightSideComponent({
setTitle,
setPosterUrl,
setIsPosterUrlCorrect,
setIsUserInteractedWithPoster,
setPosterList,
isTorrentSourceCorrect,
title,
posterUrl,
isPosterUrlCorrect,
posterList,
currentLang,
posterSearchLanguage,
setPosterSearchLanguage,
posterSearch,
removePoster,
torrentSource,
}) {
const { t } = useTranslation()
const handleTitleChange = ({ target: { value } }) => setTitle(value)
const handlePosterUrlChange = ({ target: { value } }) => {
setPosterUrl(value)
checkImageURL(value).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(!!value)
setPosterList()
}
const userChangesPosterUrl = url => {
setPosterUrl(url)
checkImageURL(url).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(true)
}
return (
<RightSide>
<RightSideContainer isHidden={!isTorrentSourceCorrect}>
<TextField onChange={handleTitleChange} value={title} margin='dense' label={t('Title')} type='text' fullWidth />
<TextField
onChange={handlePosterUrlChange}
value={posterUrl}
margin='dense'
label={t('AddPosterLinkInput')}
type='url'
fullWidth
/>
<PosterWrapper>
<Poster poster={+isPosterUrlCorrect}>
{isPosterUrlCorrect ? <img src={posterUrl} alt='poster' /> : <NoImageIcon />}
</Poster>
<PosterSuggestions>
{posterList
?.filter(url => url !== posterUrl)
.slice(0, 12)
.map(url => (
<PosterSuggestionsItem onClick={() => userChangesPosterUrl(url)} key={uuidv4()}>
<img src={url} alt='poster' />
</PosterSuggestionsItem>
))}
</PosterSuggestions>
{currentLang !== 'en' && (
<PosterLanguageSwitch
onClick={() => {
const newLanguage = posterSearchLanguage === 'en' ? 'ru' : 'en'
setPosterSearchLanguage(newLanguage)
posterSearch(title, newLanguage, { shouldRefreshMainPoster: true })
}}
showbutton={+isPosterUrlCorrect}
color='primary'
variant='contained'
size='small'
>
{posterSearchLanguage === 'en' ? 'EN' : 'RU'}
</PosterLanguageSwitch>
)}
<ClearPosterButton
showbutton={+isPosterUrlCorrect}
onClick={() => {
removePoster()
setIsUserInteractedWithPoster(true)
}}
color='primary'
variant='contained'
size='small'
>
{t('Clear')}
</ClearPosterButton>
</PosterWrapper>
</RightSideContainer>
<RightSideContainer
isError={torrentSource && !isTorrentSourceCorrect}
notificationMessage={
!torrentSource ? t('AddTorrentSourceNotification') : !isTorrentSourceCorrect && t('WrongTorrentSource')
}
isHidden={isTorrentSourceCorrect}
/>
</RightSide>
)
}

View File

@@ -1,10 +1,19 @@
import axios from 'axios' import axios from 'axios'
import parseTorrent from 'parse-torrent'
import ptt from 'parse-torrent-title'
export const getMoviePosters = (movieName, language = 'en') => { export const getMoviePosters = (movieName, language = 'en') => {
const request = `${`http://api.themoviedb.org/3/search/multi?api_key=${process.env.REACT_APP_TMDB_API_KEY}`}&language=${language}&include_image_language=${language},null&query=${movieName}` const url = 'http://api.themoviedb.org/3/search/multi'
return axios return axios
.get(request) .get(url, {
params: {
api_key: process.env.REACT_APP_TMDB_API_KEY,
language,
include_image_language: `${language},null`,
query: movieName,
},
})
.then(({ data: { results } }) => .then(({ data: { results } }) =>
results.filter(el => el.poster_path).map(el => `https://image.tmdb.org/t/p/w300${el.poster_path}`), results.filter(el => el.poster_path).map(el => `https://image.tmdb.org/t/p/w300${el.poster_path}`),
) )
@@ -27,3 +36,20 @@ const hashRegex = /^\b[0-9a-f]{32}\b$|^\b[0-9a-f]{40}\b$|^\b[0-9a-f]{64}\b$/i
const torrentRegex = /^.*\.(torrent)$/i const torrentRegex = /^.*\.(torrent)$/i
export const chechTorrentSource = source => export const chechTorrentSource = source =>
source.match(hashRegex) !== null || source.match(magnetRegex) !== null || source.match(torrentRegex) !== null source.match(hashRegex) !== null || source.match(magnetRegex) !== null || source.match(torrentRegex) !== null
export const parseTorrentTitle = (parsingSource, callback) => {
parseTorrent.remote(parsingSource, (err, { name, files } = {}) => {
if (!name || err) return callback(null)
const torrentName = ptt.parse(name).title
const nameOfFileInsideTorrent = files ? ptt.parse(files[0].name).title : null
let newTitle = torrentName
if (nameOfFileInsideTorrent) {
// taking shorter title because in most cases it is more accurate
newTitle = torrentName.length < nameOfFileInsideTorrent.length ? torrentName : nameOfFileInsideTorrent
}
callback(newTitle)
})
}

View File

@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react'
export default function usePreviousState(value) {
const ref = useRef(value)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}