diff --git a/web/.env_example b/web/.env_example index 166e2b4..9a8259f 100644 --- a/web/.env_example +++ b/web/.env_example @@ -1 +1,2 @@ -REACT_APP_SERVER_HOST= \ No newline at end of file +REACT_APP_SERVER_HOST= +REACT_APP_TMDB_API_KEY= \ No newline at end of file diff --git a/web/package.json b/web/package.json index 51d7989..2a23206 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,13 @@ "konva": "^8.0.1", "lodash": "^4.17.21", "material-ui-image": "^3.3.2", + "parse-torrent": "^9.1.3", "parse-torrent-title": "^1.3.0", "react": "^17.0.2", "react-copy-to-clipboard": "^5.0.3", "react-div-100vh": "^0.6.0", "react-dom": "^17.0.2", + "react-dropzone": "^11.3.2", "react-i18next": "^11.10.0", "react-konva": "^17.0.2-4", "react-measure": "^2.5.2", diff --git a/web/src/App/Sidebar.jsx b/web/src/App/Sidebar.jsx index a999a84..c2fa30f 100644 --- a/web/src/App/Sidebar.jsx +++ b/web/src/App/Sidebar.jsx @@ -7,7 +7,6 @@ import AddDialogButton from 'components/Add' import RemoveAll from 'components/RemoveAll' import SettingsDialog from 'components/Settings' import AboutDialog from 'components/About' -import UploadDialog from 'components/Upload' import { CreditCard as CreditCardIcon, List as ListIcon, Language as LanguageIcon } from '@material-ui/icons' import List from '@material-ui/core/List' import CloseServer from 'components/CloseServer' @@ -24,7 +23,6 @@ export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index a1dd0c2..f077d91 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -1,55 +1,308 @@ -import { useState } from 'react' +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 DialogActions from '@material-ui/core/DialogActions' -import DialogContent from '@material-ui/core/DialogContent' -import DialogTitle from '@material-ui/core/DialogTitle' -import { torrentsHost } from 'utils/Hosts' +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 { + ButtonWrapper, + CancelIconWrapper, + ClearPosterButton, + PosterLanguageSwitch, + Content, + Header, + IconWrapper, + RightSide, + Poster, + PosterSuggestions, + PosterSuggestionsItem, + PosterWrapper, + LeftSide, + LeftSideBottomSectionFileSelected, + LeftSideBottomSectionNoFile, + LeftSideTopSection, + TorrentIconWrapper, + RightSideContainer, +} from './style' +import { checkImageURL, getMoviePosters, chechTorrentSource } from './helpers' export default function AddDialog({ handleClose }) { const { t } = useTranslation() - const [link, setLink] = useState('') + const [torrentSource, setTorrentSource] = useState('') + const [isTorrentSourceActive, setIsTorrentSourceActive] = useState(false) const [title, setTitle] = useState('') - const [poster, setPoster] = 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 inputMagnet = ({ target: { value } }) => setLink(value) - const inputTitle = ({ target: { value } }) => setTitle(value) - const inputPoster = ({ target: { value } }) => setPoster(value) + const fullScreen = useMediaQuery('@media (max-width:930px)') + + const posterSearch = useMemo( + () => + (movieName, language, settings = {}) => { + const { shouldRefreshMainPoster = false } = settings + getMoviePosters(movieName, language).then(urlList => { + if (urlList) { + setPosterList(urlList) + if (!shouldRefreshMainPoster && isUserInteractedWithPoster) return + + const [firstPoster] = urlList + checkImageURL(firstPoster).then(correctImage => { + if (correctImage) { + setIsPosterUrlCorrect(true) + setPosterUrl(firstPoster) + } else removePoster() + }) + } else { + setPosterList() + if (isUserInteractedWithPoster) return + + removePoster() + } + }) + }, + [isUserInteractedWithPoster], + ) + + 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]) + + useEffect(() => { + setIsTorrentSourceCorrect(chechTorrentSource(torrentSource)) + }, [torrentSource]) + + 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 = () => { + 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() + } const handleSave = () => { - axios.post(torrentsHost(), { action: 'add', link, title, poster, save_to_db: true }).finally(() => handleClose()) + if (selectedFile) { + // file save + 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 { + // link save + axios + .post(torrentsHost(), { action: 'add', link: torrentSource, title, poster: posterUrl, save_to_db: true }) + .finally(handleClose) + } + } + + const clearSelectedFile = () => { + setSelectedFile() + setTorrentSource('') + setIsUserInteractedWithTitle(false) + } + + const userChangesPosterUrl = url => { + setPosterUrl(url) + checkImageURL(url).then(setIsPosterUrlCorrect) + setIsUserInteractedWithPoster(true) } return ( - - {t('AddMagnetOrLink')} + +
{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/helpers.js b/web/src/components/Add/helpers.js new file mode 100644 index 0000000..7203d9e --- /dev/null +++ b/web/src/components/Add/helpers.js @@ -0,0 +1,29 @@ +import axios from 'axios' + +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}` + + 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) +} + +export 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 + } +} + +const magnetRegex = /^magnet:\?xt=urn:[a-z0-9].*/i +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 diff --git a/web/src/components/Add/style.js b/web/src/components/Add/style.js new file mode 100644 index 0000000..5374a5f --- /dev/null +++ b/web/src/components/Add/style.js @@ -0,0 +1,296 @@ +import { Button } from '@material-ui/core' +import styled, { css } from 'styled-components' + +export 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; +` + +export const Content = styled.div` + background: linear-gradient(145deg, #e4f6ed, #b5dec9); + flex: 1; + display: grid; + grid-template-columns: repeat(2, 1fr); + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + overflow: auto; + + @media (max-width: 930px) { + grid-template-columns: 1fr; + } +` + +export const RightSide = styled.div` + padding: 0 20px 20px 20px; +` + +export const RightSideContainer = styled.div` + ${({ isHidden, notificationMessage, isError }) => css` + height: 455px; + + ${notificationMessage && + css` + position: relative; + white-space: nowrap; + + :before { + font-size: 20px; + font-weight: 300; + content: '${notificationMessage}'; + display: grid; + place-items: center; + background: ${isError ? '#cda184' : '#84cda7'}; + padding: 10px 15px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 5px; + } + `}; + + ${isHidden && + css` + display: none; + `}; + `} +` +export const LeftSide = styled.div` + display: flex; + flex-direction: column; + border-right: 1px solid rgba(0, 0, 0, 0.12); +` + +export const LeftSideBottomSectionBasicStyles = css` + transition: transform 0.3s; + padding: 20px; + height: 100%; + display: grid; +` + +export const LeftSideBottomSectionNoFile = styled.div` + ${LeftSideBottomSectionBasicStyles} + border: 4px dashed rgba(0,0,0,0.1); + text-align: center; + + ${({ 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%); + } + } + + @media (max-width: 930px) { + border: 4px dashed transparent; + height: 400px; + place-items: center; + grid-template-rows: 40% 1fr; + } +` + +export const LeftSideBottomSectionFileSelected = styled.div` + ${LeftSideBottomSectionBasicStyles} + place-items: center; + + @media (max-width: 930px) { + height: 400px; + } +` + +export const TorrentIconWrapper = styled.div` + position: relative; +` + +export 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); + } + } +` + +export const IconWrapper = styled.div` + display: grid; + justify-items: center; + align-content: start; + gap: 10px; + align-self: start; + + svg { + transition: all 0.3s; + } +` + +export const LeftSideTopSection = styled.div` + background: #e3f2eb; + padding: 0 20px 20px 20px; + transition: all 0.3s; + + ${({ active }) => active && 'box-shadow: 0 8px 10px -9px rgba(0, 0, 0, 0.5)'}; +` + +export const PosterWrapper = styled.div` + margin-top: 20px; + display: grid; + grid-template-columns: max-content 1fr; + grid-template-rows: 300px max-content; + column-gap: 5px; + position: relative; + margin-bottom: 20px; + + grid-template-areas: + 'poster suggestions' + 'clear empty'; + + @media (max-width: 540px) { + grid-template-columns: 1fr; + gap: 5px 0; + justify-items: center; + grid-template-areas: + 'poster' + 'clear' + 'suggestions'; + } +` +export const PosterSuggestions = styled.div` + display: grid; + grid-area: suggestions; + grid-auto-flow: column; + grid-template-columns: repeat(3, max-content); + grid-template-rows: repeat(4, max-content); + gap: 5px; + + @media (max-width: 540px) { + grid-auto-flow: row; + grid-template-columns: repeat(5, max-content); + } + @media (max-width: 375px) { + grid-template-columns: repeat(4, max-content); + } +` +export const PosterSuggestionsItem = styled.div` + cursor: pointer; + width: 71px; + height: 71px; + + @media (max-width: 430px) { + width: 60px; + height: 60px; + } + + @media (max-width: 375px) { + width: 71px; + height: 71px; + } + + @media (max-width: 355px) { + width: 60px; + height: 60px; + } + + 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; + grid-area: poster; + + ${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); + } + `} + `} +` + +export const ClearPosterButton = styled(Button)` + grid-area: clear; + justify-self: center; + transform: translateY(-50%); + position: absolute; + ${({ showbutton }) => !showbutton && 'display: none'}; + + @media (max-width: 540px) { + transform: translateY(-140%); + } +` + +export const PosterLanguageSwitch = styled.div` + grid-area: poster; + z-index: 5; + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + background: #74c39c; + border-radius: 50%; + display: grid; + place-items: center; + color: #e1f4eb; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + + ${({ showbutton }) => !showbutton && 'display: none'}; + + :hover { + filter: brightness(1.1); + } +` + +export const ButtonWrapper = styled.div` + padding: 20px; + display: flex; + justify-content: flex-end; + + > :not(:last-child) { + margin-right: 10px; + } +` diff --git a/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx index a65a7e5..aa037ed 100644 --- a/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/DetailedView/index.jsx @@ -11,14 +11,14 @@ import { DownlodSpeedWidget, } from '../widgets' -export default function Test({ +export default function DetailedView({ downloadSpeed, uploadSpeed, torrent, torrentSize, PiecesCount, PiecesLength, - statString, + stat, cache, }) { return ( @@ -32,7 +32,7 @@ export default function Test({ - + diff --git a/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent b/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent new file mode 100644 index 0000000..099c998 Binary files /dev/null and b/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent differ diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx index e1cdd42..f4ee8b7 100644 --- a/web/src/components/DialogTorrentDetailsContent/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -54,7 +54,6 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { stat, download_speed: downloadSpeed, upload_speed: uploadSpeed, - stat_string: statString, torrent_size: torrentSize, file_stats: torrentFileList, } = torrent @@ -133,7 +132,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { torrentSize={torrentSize} PiecesCount={PiecesCount} PiecesLength={PiecesLength} - statString={statString} + stat={stat} cache={cache} /> ) : ( @@ -156,7 +155,7 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { - + diff --git a/web/src/components/DialogTorrentDetailsContent/widgets.jsx b/web/src/components/DialogTorrentDetailsContent/widgets.jsx index c3d65e3..a4a1437 100644 --- a/web/src/components/DialogTorrentDetailsContent/widgets.jsx +++ b/web/src/components/DialogTorrentDetailsContent/widgets.jsx @@ -9,6 +9,7 @@ import { } from '@material-ui/icons' import { getPeerString, humanizeSize } from 'utils/Utils' import { useTranslation } from 'react-i18next' +import { GETTING_INFO, IN_DB, CLOSED, PRELOAD, WORKING } from 'torrentStates' import StatisticsField from './StatisticsField' @@ -69,22 +70,26 @@ export const PiecesLengthWidget = ({ data }) => { ) } -export const StatusWidget = ({ data }) => { +export const StatusWidget = ({ stat }) => { const { t } = useTranslation() - let i18nd = data - if (data.toLowerCase() === 'torrent added') - i18nd = t('TorrentAdded') - else if (data.toLowerCase() === 'torrent getting info') - i18nd = t('TorrentGettingInfo') - else if (data.toLowerCase() === 'torrent preload') - i18nd = t('TorrentPreload') - else if (data.toLowerCase() === 'torrent working') - i18nd = t('TorrentWorking') - else if (data.toLowerCase() === 'torrent closed') - i18nd = t('TorrentClosed') - else if (data.toLowerCase() === 'torrent in db') - i18nd = t('TorrentInDb') - return + + const values = { + [GETTING_INFO]: t('TorrentGettingInfo'), + [PRELOAD]: t('TorrentPreload'), + [WORKING]: t('TorrentWorking'), + [CLOSED]: t('TorrentClosed'), + [IN_DB]: t('TorrentInDb'), + } + + return ( + + ) } export const SizeWidget = ({ data }) => { diff --git a/web/src/components/Upload.jsx b/web/src/components/Upload.jsx deleted file mode 100644 index 8f460d8..0000000 --- a/web/src/components/Upload.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemText from '@material-ui/core/ListItemText' -import ListItem from '@material-ui/core/ListItem' -import PublishIcon from '@material-ui/icons/Publish' -import { torrentUploadHost } from 'utils/Hosts' -import axios from 'axios' -import { useTranslation } from 'react-i18next' - -export default function UploadDialog() { - const { t } = useTranslation() - const handleCapture = ({ target: { files } }) => { - const [file] = files - const data = new FormData() - data.append('save', 'true') - data.append('file', file) - axios.post(torrentUploadHost(), data) - } - return ( -
- -
- ) -} diff --git a/web/src/icons/index.jsx b/web/src/icons/index.jsx index f454f2b..f8c792e 100644 --- a/web/src/icons/index.jsx +++ b/web/src/icons/index.jsx @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const NoImageIcon = () => ( ( ) + +export const AddItemIcon = () => ( + + + + + + +) + +export const TorrentIcon = () => ( + + + + + + + + + + + + + + + + + + + + +) diff --git a/web/src/locales/en/translation.json b/web/src/locales/en/translation.json index 6965d12..4e8acca 100644 --- a/web/src/locales/en/translation.json +++ b/web/src/locales/en/translation.json @@ -3,7 +3,7 @@ "Actions": "Actions", "Add": "Add", "AddFromLink": "Add from Link", - "AddMagnetOrLink": "Add magnet or link to torrent file", + "AddNewTorrent": "Add new torrent", "AddRetrackers": "Add retrackers", "Buffer": "Preload Buffer / Cache", "BufferNote": "Enable “Preload Buffer” in settings to see cache loading progress", @@ -39,7 +39,7 @@ "Host": "Host", "Info": "Info", "LatestFilePlayed": "Latest file played:", - "MagnetOrTorrentFileLink": "Magnet or torrent file link", + "TorrentSourceLink": "Torrent source link", "Name": "Name", "NoTorrentsAdded": "No torrents added", "Offline": "Offline", @@ -51,7 +51,7 @@ "PiecesCount": "Pieces count", "PiecesLength": "Pieces length", "PlaylistAll": "Playlist All", - "Poster": "Poster", + "AddPosterLinkInput": "Poster link", "Preload": "Preload", "PreloadBuffer": "Preload Buffer", "ReaderReadAHead": "Reader Read Ahead (5-100%)", @@ -62,7 +62,7 @@ "RemoveViews": "Remove View States", "ReplaceRetrackers": "Replace retrackers", "Resolution": "Resolution", - "RetrackersMode": "Retrackers Mode", + "RetrackersMode": "Retrackers Mode", "Save": "Save", "Season": "Season", "SelectSeason": "Select Season", @@ -96,5 +96,13 @@ "UseDisk": "Use Disk for Cache", "UseDiskDesc": "Better use external media on flash-based devices", "UTP": "μTP (Micro Transport Protocol)", - "Viewed": "Viewed" + "Viewed": "Viewed", + "AppendFile": { + "Or": "OR", + "ClickOrDrag": "CLICK / DRAG & DROP (.torrent)" + }, + "TorrentSourceOptions": "magnet / hash / .torrent file link", + "Clear": "Clear", + "AddTorrentSourceNotification": "First add your torrent source", + "WrongTorrentSource": "Wrong torrent source" } \ No newline at end of file diff --git a/web/src/locales/ru/translation.json b/web/src/locales/ru/translation.json index 181c73c..fefc90e 100644 --- a/web/src/locales/ru/translation.json +++ b/web/src/locales/ru/translation.json @@ -3,7 +3,7 @@ "Actions": "Действия", "Add": "Добавить", "AddFromLink": "Добавить", - "AddMagnetOrLink": "Добавьте magnet или ссылку на торрент", + "AddNewTorrent": "Добавить новый торрент", "AddRetrackers": "Добавлять", "Buffer": "Предзагрузка / Кеш", "BufferNote": "Включите «Наполнять кеш перед началом воспроизведения» в настройках для показа заполнения кеша", @@ -39,7 +39,7 @@ "Host": "Хост", "Info": "Инфо", "LatestFilePlayed": "Последний воспроизведенный файл:", - "MagnetOrTorrentFileLink": "Ссылка на файл торрента или magnet-ссылка", + "TorrentSourceLink": "Ссылка на источник торрента", "Name": "Название", "NoTorrentsAdded": "Нет торрентов", "Offline": "Сервер не доступен", @@ -51,7 +51,7 @@ "PiecesCount": "Кол-во блоков", "PiecesLength": "Размер блока", "PlaylistAll": "Плейлист всех", - "Poster": "Постер", + "AddPosterLinkInput": "Ссылка на постер", "Preload": "Предзагр.", "PreloadBuffer": "Наполнять кеш перед началом воспроизведения", "ReaderReadAHead": "Кеш предзагрузки (5-100%, рек. 95%)", @@ -96,5 +96,13 @@ "UseDisk": "Использовать диск для кеша", "UseDiskDesc": "Рекомендуется использовать внешние носители на устройствах с flash-памятью", "UTP": "μTP (Micro Transport Protocol)", - "Viewed": "Просм." + "Viewed": "Просм.", + "AppendFile": { + "Or": "ИЛИ", + "ClickOrDrag": "НАЖМИТЕ / ПЕРЕТАЩИТЕ ФАЙЛ (.torrent)" + }, + "TorrentSourceOptions": "magnet ссылка / хеш / ссылка на .torrent файл", + "Clear": "Очистить", + "AddTorrentSourceNotification": "Сначала добавьте торрент источник", + "WrongTorrentSource": "Неправильный torrent источник" } \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index ab99e80..bb34a6a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2730,6 +2730,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^9.6.1: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" @@ -3218,6 +3223,18 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= +bencode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.1.tgz#667a6a31c5e038d558608333da6b7c94e836c85b" + integrity sha512-2uhEl8FdjSBUyb69qDTgOEeeqDTa+n3yMQzLW0cOzNf1Ow5bwcg3idf+qsWisIKRH8Bk8oC7UXL8irRcPA8ZEQ== + dependencies: + safe-buffer "^5.1.1" + +bep53-range@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-1.1.0.tgz#a009311710c955d27eb3a30cf329e8c139693d27" + integrity sha512-yGQTG4NtwTciX0Bkgk1FqQL4p+NiCQKpTSFho2lrxvUkXIlzyJDwraj8aYxAxRZMnnOhRr7QlIBoMRPEnIR34Q== + bfj@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -3255,6 +3272,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +blob-to-buffer@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" + integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== + bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -4523,6 +4545,13 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -5591,6 +5620,13 @@ file-loader@6.1.1: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80" + integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA== + dependencies: + tslib "^2.0.3" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -5956,6 +5992,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -8131,6 +8172,14 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +magnet-uri@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.2.0.tgz#10f7be050bf23452df210838239b118463c3eeff" + integrity sha512-O9AgdDwT771fnUj0giPYu/rACpz8173y8UXCSOdLITjOVfBenZ9H9q3FqQmveK+ORUMuD+BkKNSZP8C3+IMAKQ== + dependencies: + bep53-range "^1.1.0" + thirty-two "^1.0.2" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -8326,6 +8375,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + mini-css-extract-plugin@0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz#15b0910a7f32e62ffde4a7430cfefbd700724ea6" @@ -9088,6 +9142,19 @@ parse-torrent-title@^1.3.0: resolved "https://registry.yarnpkg.com/parse-torrent-title/-/parse-torrent-title-1.3.0.tgz#3dedea10277b17998b124a4fd67d9e190b0306b8" integrity sha512-R5wya73/Ef0qUhb9177Ko8nRQyN1ziWD5DPnlrDrrgcchUnmIrG//cPENunvFYRZCLDZosXTKTo7TpQ2Pgbryg== +parse-torrent@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.3.tgz#9b4bc8dca243b356bf449938d6d38a259a2a707c" + integrity sha512-/Yr951CvJM8S6TjMaqrsmMxeQEAjDeCX+MZ3hGXXc7DG2wqzp/rzOsHtDzIVqN6NsFRCqy6wYLF/W7Sgvq7bXw== + dependencies: + bencode "^2.0.1" + blob-to-buffer "^1.2.9" + get-stdin "^8.0.0" + magnet-uri "^6.0.0" + queue-microtask "^1.2.2" + simple-get "^4.0.0" + simple-sha1 "^3.0.1" + parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -10322,6 +10389,15 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-dropzone@^11.3.2: + version "11.3.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.3.2.tgz#2efb6af800a4779a9daa1e7ba1f8d51d0ab862d7" + integrity sha512-Z0l/YHcrNK1r85o6RT77Z5XgTARmlZZGfEKBl3tqTXL9fZNQDuIdRx/J0QjvR60X+yYu26dnHeaG2pWU+1HHvw== + dependencies: + attr-accept "^2.2.1" + file-selector "^0.2.2" + prop-types "^15.7.2" + react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -10962,6 +11038,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rusha@^0.8.13: + version "0.8.14" + resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68" + integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA== + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -11255,6 +11336,28 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" + integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-sha1@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" + integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== + dependencies: + queue-microtask "^1.2.2" + rusha "^0.8.13" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -11977,6 +12080,11 @@ textextensions@^3.2.0: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-3.3.0.tgz#03530d5287b86773c08b77458589148870cc71d3" integrity sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw== +thirty-two@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" + integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno= + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"