Files
TorrServerJellyfin/web/src/components/Add/AddDialog.jsx
Daniel Shleifman 763b55c75e box-shadow fixed
2021-06-09 01:42:44 +03:00

421 lines
11 KiB
JavaScript

import { useCallback, 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 styled, { css } from 'styled-components'
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'
const Header = styled.div`
background: #00a572;
color: rgba(0, 0, 0, 0.87);
font-size: 20px;
color: #fff;
font-weight: 500;
box-shadow: 0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
padding: 15px 24px;
position: relative;
`
const ContentWrapper = styled.div`
background: linear-gradient(145deg, #e4f6ed, #b5dec9);
display: flex;
flex-direction: column;
justify-content: space-between;
`
const Content = styled.div`
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
`
const LeftSide = styled.div`
padding: 0 20px 20px 20px;
border-right: 1px solid rgba(0, 0, 0, 0.12);
`
const RightSide = styled.div`
display: flex;
flex-direction: column;
`
const RightSideBottomSectionBasicStyles = css`
transition: transform 0.3s;
padding: 20px;
height: 100%;
display: grid;
`
const RightSideBottomSectionNoFile = styled.div`
${RightSideBottomSectionBasicStyles}
border: 4px dashed transparent;
${({ isDragActive }) => isDragActive && `border: 4px dashed green`};
justify-items: center;
grid-template-rows: 100px 1fr;
cursor: pointer;
:hover {
background-color: rgba(0, 0, 0, 0.04);
svg {
transform: translateY(-4%);
}
}
`
const RightSideBottomSectionFileSelected = styled.div`
${RightSideBottomSectionBasicStyles}
place-items: center;
`
const TorrentIconWrapper = styled.div`
position: relative;
`
const CancelIconWrapper = styled.div`
position: absolute;
top: -9px;
left: 10px;
cursor: pointer;
> svg {
transition: all 0.3s;
fill: rgba(0, 0, 0, 0.7);
:hover {
fill: rgba(0, 0, 0, 0.6);
}
}
`
const IconWrapper = styled.div`
display: grid;
justify-items: center;
align-content: start;
gap: 10px;
svg {
transition: all 0.3s;
}
`
const RightSideTopSection = styled.div`
background: #fff;
padding: 0 20px 20px 20px;
transition: all 0.3s;
${({ active }) => active && 'box-shadow: 0 8px 10px -9px rgba(0, 0, 0, 0.5)'};
`
const PosterWrapper = styled.div`
margin-top: 20px;
display: grid;
grid-template-columns: max-content 1fr;
grid-template-rows: 300px max-content;
gap: 10px 5px;
`
const PosterSuggestions = styled.div`
display: grid;
grid-template-columns: repeat(3, 70px);
grid-template-rows: repeat(4, calc(25% - 4px));
grid-auto-flow: column;
gap: 5px;
`
const PosterSuggestionsItem = styled.div`
cursor: pointer;
img {
transition: all 0.3s;
border-radius: 5px;
width: 100%;
height: 100%;
object-fit: cover;
:hover {
filter: brightness(130%);
}
}
`
export const Poster = styled.div`
${({ poster }) => css`
border-radius: 5px;
overflow: hidden;
width: 200px;
${poster
? css`
img {
width: 200px;
object-fit: cover;
border-radius: 5px;
height: 100%;
}
`
: css`
display: grid;
place-items: center;
background: #74c39c;
svg {
transform: scale(1.5) translateY(-3px);
}
`}
${
'' /* @media (max-width: 1280px) {
align-self: start;
}
@media (max-width: 840px) {
${poster
? css`
height: 200px;
`
: css`
display: none;
`}
} */
}
`}
`
const ButtonWrapper = styled.div`
padding: 20px;
display: flex;
justify-content: flex-end;
> :not(:last-child) {
margin-right: 10px;
}
`
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}`
return axios
.get(request)
.then(({ data: { results } }) =>
results.filter(el => el.poster_path).map(el => `https://image.tmdb.org/t/p/w300${el.poster_path}`),
)
.catch(() => null)
}
const checkImageURL = async url => {
if (!url || !url.match(/.(jpg|jpeg|png|gif)$/i)) return false
try {
await fetch(url)
return true
} catch (e) {
return false
}
}
export default function AddDialog({ handleClose }) {
const { t } = useTranslation()
const [torrentSource, setTorrentSource] = useState('')
const [torrentSourceSelected, setTorrentSourceSelected] = useState(false)
const [title, setTitle] = useState('')
const [posterUrl, setPosterUrl] = useState('')
const [isPosterUrlCorrect, setIsPosterUrlCorrect] = useState(false)
const [posterList, setPosterList] = useState()
const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(false)
const [currentLang] = useChangeLanguage()
const [selectedFile, setSelectedFile] = useState()
const handleCapture = useCallback(files => {
const [file] = files
if (!file) return
setSelectedFile(file)
setTorrentSource(file.name)
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop: handleCapture, accept: '.torrent' })
const removePoster = () => {
setIsPosterUrlCorrect(false)
setPosterUrl('')
}
const delayedPosterSearch = useMemo(
() =>
debounce(movieName => {
getMoviePosters(movieName, currentLang === 'ru' ? 'ru' : 'en').then(urlList => {
if (urlList) {
setPosterList(urlList)
if (isUserInteractedWithPoster) return
const [firstPoster] = urlList
checkImageURL(firstPoster).then(correctImage => {
if (correctImage) {
setIsPosterUrlCorrect(true)
setPosterUrl(firstPoster)
} else removePoster()
})
} else {
setPosterList()
if (isUserInteractedWithPoster) return
removePoster()
}
})
}, 700),
[isUserInteractedWithPoster, currentLang],
)
const handleTorrentSourceChange = ({ target: { value } }) => setTorrentSource(value)
const handleTitleChange = ({ target: { value } }) => {
setTitle(value)
delayedPosterSearch(value)
}
const handlePosterUrlChange = ({ target: { value } }) => {
setPosterUrl(value)
checkImageURL(value).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(!!value)
setPosterList()
}
const handleSave = () => {
if (selectedFile) {
const data = new FormData()
data.append('save', 'true')
data.append('file', selectedFile)
title && data.append('title', title)
posterUrl && data.append('poster', posterUrl)
axios.post(torrentUploadHost(), data).finally(() => handleClose())
} else {
axios
.post(torrentsHost(), { action: 'add', link: torrentSource, title, poster: posterUrl, save_to_db: true })
.finally(() => handleClose())
}
}
const clearSelectedFile = () => {
setSelectedFile()
setTorrentSource('')
}
const userChangesPosterUrl = url => {
setPosterUrl(url)
checkImageURL(url).then(setIsPosterUrlCorrect)
setIsUserInteractedWithPoster(true)
}
return (
<Dialog open onClose={handleClose} aria-labelledby='form-dialog-title' fullWidth maxWidth='md'>
<Header>{t('AddNewTorrent')}</Header>
<ContentWrapper>
<Content>
<LeftSide>
<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>
<Button
style={{ justifySelf: 'center' }}
onClick={removePoster}
color='primary'
variant='outlined'
size='small'
disabled={!posterUrl}
>
{t('Clear')}
</Button>
</PosterWrapper>
</LeftSide>
<RightSide>
<RightSideTopSection active={torrentSourceSelected}>
<TextField
onChange={handleTorrentSourceChange}
value={torrentSource}
margin='dense'
label={t('TorrentSourceLink')}
helperText={t('TorrentSourceOptions')}
type='text'
fullWidth
onFocus={() => setTorrentSourceSelected(true)}
onBlur={() => setTorrentSourceSelected(false)}
inputProps={{ autoComplete: 'off' }}
disabled={!!selectedFile}
/>
</RightSideTopSection>
{selectedFile ? (
<RightSideBottomSectionFileSelected>
<TorrentIconWrapper>
<TorrentIcon />
<CancelIconWrapper onClick={clearSelectedFile}>
<CancelIcon />
</CancelIconWrapper>
</TorrentIconWrapper>
</RightSideBottomSectionFileSelected>
) : (
<RightSideBottomSectionNoFile isDragActive={isDragActive} {...getRootProps()}>
<input {...getInputProps()} />
<div>{t('AppendFile.Or')}</div>
<IconWrapper>
<AddItemIcon color='primary' />
<div>{t('AppendFile.ClickOrDrag')}</div>
</IconWrapper>
</RightSideBottomSectionNoFile>
)}
</RightSide>
</Content>
<ButtonWrapper>
<Button onClick={handleClose} color='primary' variant='outlined'>
{t('Cancel')}
</Button>
<Button variant='contained' disabled={!torrentSource} onClick={handleSave} color='primary'>
{t('Add')}
</Button>
</ButtonWrapper>
</ContentWrapper>
</Dialog>
)
}