diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index 678c231..6e5bdbf 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -1,65 +1,46 @@ import { useEffect, useMemo, useState } from 'react' import Button from '@material-ui/core/Button' -import TextField from '@material-ui/core/TextField' import Dialog from '@material-ui/core/Dialog' import { torrentsHost, torrentUploadHost } from 'utils/Hosts' import axios from 'axios' import { useTranslation } from 'react-i18next' -import { NoImageIcon, AddItemIcon, TorrentIcon } from 'icons' import debounce from 'lodash/debounce' -import { v4 as uuidv4 } from 'uuid' 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 parseTorrent from 'parse-torrent' -import ptt from 'parse-torrent-title' import CircularProgress from '@material-ui/core/CircularProgress' +import usePreviousState from 'utils/usePreviousState' -import { checkImageURL, getMoviePosters, chechTorrentSource } from './helpers' -import { - ButtonWrapper, - CancelIconWrapper, - ClearPosterButton, - PosterLanguageSwitch, - Content, - Header, - IconWrapper, - RightSide, - Poster, - PosterSuggestions, - PosterSuggestionsItem, - PosterWrapper, - LeftSide, - LeftSideBottomSectionFileSelected, - LeftSideBottomSectionNoFile, - LeftSideTopSection, - TorrentIconWrapper, - RightSideContainer, -} from './style' +import { checkImageURL, getMoviePosters, chechTorrentSource, parseTorrentTitle } from './helpers' +import { ButtonWrapper, Content, Header } from './style' +import RightSideComponent from './RightSideComponent' +import LeftSideComponent from './LeftSideComponent' export default function AddDialog({ handleClose }) { const { t } = useTranslation() const [torrentSource, setTorrentSource] = useState('') - const [isTorrentSourceActive, setIsTorrentSourceActive] = useState(false) const [title, setTitle] = useState('') const [posterUrl, setPosterUrl] = useState('') const [isPosterUrlCorrect, setIsPosterUrlCorrect] = useState(false) const [isTorrentSourceCorrect, setIsTorrentSourceCorrect] = useState(false) const [posterList, setPosterList] = useState() const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(false) - const [isUserInteractedWithTitle, setIsUserInteractedWithTitle] = useState(false) const [currentLang] = useChangeLanguage() const [selectedFile, setSelectedFile] = useState() const [posterSearchLanguage, setPosterSearchLanguage] = useState(currentLang === 'ru' ? 'ru' : 'en') const [isLoadingButton, setIsLoadingButton] = useState(false) + const [skipDebounce, setSkipDebounce] = useState(false) const fullScreen = useMediaQuery('@media (max-width:930px)') const posterSearch = useMemo( () => - (movieName, language, settings = {}) => { - const { shouldRefreshMainPoster = false } = settings + (movieName, language, { shouldRefreshMainPoster = false } = {}) => { + if (!movieName) { + setPosterList() + removePoster() + return + } + getMoviePosters(movieName, language).then(urlList => { if (urlList) { setPosterList(urlList) @@ -85,65 +66,54 @@ export default function AddDialog({ handleClose }) { const delayedPosterSearch = useMemo(() => debounce(posterSearch, 700), [posterSearch]) - useEffect(() => { - if (isUserInteractedWithTitle) return - - parseTorrent.remote(selectedFile || torrentSource, (err, parsedTorrent) => { - if (err) throw err - if (!parsedTorrent.name) return - - const torrentName = ptt.parse(parsedTorrent.name).title - const fileInsideTorrentName = parsedTorrent.files ? ptt.parse(parsedTorrent.files[0].name).title : null - - let value = torrentName - if (fileInsideTorrentName) { - value = torrentName.length < fileInsideTorrentName.length ? torrentName : fileInsideTorrentName - } - - setTitle(value) - delayedPosterSearch(value, posterSearchLanguage) - }) - }, [selectedFile, delayedPosterSearch, torrentSource, posterSearchLanguage, isUserInteractedWithTitle]) + const prevTitleState = usePreviousState(title) + const prevTorrentSourceState = usePreviousState(torrentSource) useEffect(() => { - setIsTorrentSourceCorrect(chechTorrentSource(torrentSource)) + // if torrentSource is updated then we are checking that source is valid and getting title from the source + const torrentSourceChanged = torrentSource !== prevTorrentSourceState - if (!torrentSource) { - setPosterUrl('') - setPosterList() - setIsPosterUrlCorrect(false) + const isCorrectSource = chechTorrentSource(torrentSource) + if (!isCorrectSource) return setIsTorrentSourceCorrect(false) + + setIsTorrentSourceCorrect(true) + + if (torrentSourceChanged) { + parseTorrentTitle(selectedFile || torrentSource, newTitle => { + if (!newTitle) return + + setSkipDebounce(true) + setTitle(newTitle) + }) } - }, [torrentSource]) + }, [prevTorrentSourceState, selectedFile, torrentSource]) - const handleCapture = files => { - const [file] = files - if (!file) return + useEffect(() => { + // if title exists and title was changed then search poster. + const titleChanged = title !== prevTitleState + if (!titleChanged) return - setIsUserInteractedWithPoster(false) - setSelectedFile(file) - setTorrentSource(file.name) - } - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: handleCapture, accept: '.torrent' }) + if (skipDebounce) { + posterSearch(title, posterSearchLanguage) + setSkipDebounce(false) + } else { + delayedPosterSearch(title, posterSearchLanguage) + } + }, [title, prevTitleState, delayedPosterSearch, posterSearch, posterSearchLanguage, skipDebounce]) const removePoster = () => { setIsPosterUrlCorrect(false) setPosterUrl('') } - const handleTorrentSourceChange = ({ target: { value } }) => setTorrentSource(value) - const handleTitleChange = ({ target: { value } }) => { - setTitle(value) - delayedPosterSearch(value, posterSearchLanguage) - - torrentSource && setIsUserInteractedWithTitle(true) - } - const handlePosterUrlChange = ({ target: { value } }) => { - setPosterUrl(value) - checkImageURL(value).then(setIsPosterUrlCorrect) - setIsUserInteractedWithPoster(!!value) - setPosterList() - } + useEffect(() => { + if (!selectedFile && !torrentSource) { + setTitle('') + setPosterList() + removePoster() + setIsUserInteractedWithPoster(false) + } + }, [selectedFile, torrentSource]) const handleSave = () => { setIsLoadingButton(true) @@ -155,10 +125,7 @@ export default function AddDialog({ handleClose }) { data.append('file', selectedFile) title && data.append('title', title) posterUrl && data.append('poster', posterUrl) - axios - .post(torrentUploadHost(), data) - // .then(res => console.log(res)) - .finally(handleClose) + axios.post(torrentUploadHost(), data).finally(handleClose) } else { // link save 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 ( {t('AddNewTorrent')} - - - setIsTorrentSourceActive(true)} - onBlur={() => setIsTorrentSourceActive(false)} - inputProps={{ autoComplete: 'off' }} - disabled={!!selectedFile} - /> - + - {selectedFile ? ( - - - - - - - - - - ) : ( - - -
{t('AppendFile.Or')}
- - - -
{t('AppendFile.ClickOrDrag')}
-
-
- )} -
- - - - - - - - - {isPosterUrlCorrect ? poster : } - - - - {posterList - ?.filter(url => url !== posterUrl) - .slice(0, 12) - .map(url => ( - userChangesPosterUrl(url)} key={uuidv4()}> - poster - - ))} - - - {currentLang !== 'en' && ( - { - 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'} - - )} - - { - removePoster() - setIsUserInteractedWithPoster(true) - }} - color='primary' - variant='contained' - size='small' - > - {t('Clear')} - - - - - - +
diff --git a/web/src/components/Add/LeftSideComponent.jsx b/web/src/components/Add/LeftSideComponent.jsx new file mode 100644 index 0000000..11c9df9 --- /dev/null +++ b/web/src/components/Add/LeftSideComponent.jsx @@ -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 ( + + + setIsTorrentSourceActive(true)} + onBlur={() => setIsTorrentSourceActive(false)} + inputProps={{ autoComplete: 'off' }} + disabled={!!selectedFile} + /> + + + {selectedFile ? ( + + + + + + + + + + ) : ( + + +
{t('AppendFile.Or')}
+ + + +
{t('AppendFile.ClickOrDrag')}
+
+
+ )} +
+ ) +} diff --git a/web/src/components/Add/RightSideComponent.jsx b/web/src/components/Add/RightSideComponent.jsx new file mode 100644 index 0000000..b76e917 --- /dev/null +++ b/web/src/components/Add/RightSideComponent.jsx @@ -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 ( + + + + + + + + {isPosterUrlCorrect ? poster : } + + + + {posterList + ?.filter(url => url !== posterUrl) + .slice(0, 12) + .map(url => ( + userChangesPosterUrl(url)} key={uuidv4()}> + poster + + ))} + + + {currentLang !== 'en' && ( + { + 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'} + + )} + + { + removePoster() + setIsUserInteractedWithPoster(true) + }} + color='primary' + variant='contained' + size='small' + > + {t('Clear')} + + + + + + + ) +} diff --git a/web/src/components/Add/helpers.js b/web/src/components/Add/helpers.js index 7203d9e..2c52395 100644 --- a/web/src/components/Add/helpers.js +++ b/web/src/components/Add/helpers.js @@ -1,10 +1,19 @@ import axios from 'axios' +import parseTorrent from 'parse-torrent' +import ptt from 'parse-torrent-title' 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 - .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 } }) => 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 export const chechTorrentSource = source => 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) + }) +} diff --git a/web/src/utils/usePreviousState.js b/web/src/utils/usePreviousState.js new file mode 100755 index 0000000..ca2a535 --- /dev/null +++ b/web/src/utils/usePreviousState.js @@ -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 +}