diff --git a/web/README.md b/web/README.md index 7d97cac..e037846 100644 --- a/web/README.md +++ b/web/README.md @@ -8,7 +8,8 @@ > `http://192.168.78.4:8090` - correct > > `http://192.168.78.4:8090/` - wrong -3. `yarn start` +3. in `.env` file add TMDB api key +4. `yarn start` ### Eslint > Prettier will fix the code every time the code is saved diff --git a/web/package.json b/web/package.json index 1dbb14a..d166c66 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,6 @@ "@material-ui/icons": "^4.11.2", "axios": "^0.21.1", "clsx": "^1.1.1", - "fontsource-roboto": "^4.0.0", "i18next": "^20.3.1", "i18next-browser-languagedetector": "^6.1.1", "lodash": "^4.17.21", diff --git a/web/public/android-chrome-192x192.png b/web/public/android-chrome-192x192.png new file mode 100644 index 0000000..d34d722 Binary files /dev/null and b/web/public/android-chrome-192x192.png differ diff --git a/web/public/android-chrome-512x512.png b/web/public/android-chrome-512x512.png new file mode 100644 index 0000000..18151cd Binary files /dev/null and b/web/public/android-chrome-512x512.png differ diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png new file mode 100644 index 0000000..e36ede4 Binary files /dev/null and b/web/public/apple-touch-icon.png differ diff --git a/web/public/browserconfig.xml b/web/public/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/web/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/web/public/favicon-16x16.png b/web/public/favicon-16x16.png new file mode 100644 index 0000000..3e592b8 Binary files /dev/null and b/web/public/favicon-16x16.png differ diff --git a/web/public/favicon-32x32.png b/web/public/favicon-32x32.png new file mode 100644 index 0000000..4153179 Binary files /dev/null and b/web/public/favicon-32x32.png differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..b234d84 Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/public/index.html b/web/public/index.html index 41b4208..7e9388a 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,33 +1,43 @@ - - - - - - - TorrServer - - - -
+ + + + + + + + + + + + + - - - - - + TorrServer + + + + +
+ + + + + + + \ No newline at end of file diff --git a/web/public/mstile-150x150.png b/web/public/mstile-150x150.png new file mode 100644 index 0000000..ac32aeb Binary files /dev/null and b/web/public/mstile-150x150.png differ diff --git a/web/public/safari-pinned-tab.svg b/web/public/safari-pinned-tab.svg new file mode 100644 index 0000000..2ad030d --- /dev/null +++ b/web/public/safari-pinned-tab.svg @@ -0,0 +1,265 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest new file mode 100644 index 0000000..52b6bb1 --- /dev/null +++ b/web/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/web/src/components/About.jsx b/web/src/components/About.jsx index 58cae8a..1bfe33e 100644 --- a/web/src/components/About.jsx +++ b/web/src/components/About.jsx @@ -29,13 +29,21 @@ export default function AboutDialog() { - setOpen(false)} aria-labelledby='form-dialog-title' fullWidth maxWidth='lg'> + setOpen(false)} + aria-labelledby='form-dialog-title' + fullWidth='true' + maxWidth='sm' + > {t('About')}

TorrServer {torrServerVersion}

- https://github.com/YouROK/TorrServer + + https://github.com/YouROK/TorrServer +
@@ -43,13 +51,25 @@ export default function AboutDialog() {

{t('SpecialThanks')}

- anacrolix Matt Joiner github.com/anacrolix + anacrolix Matt Joiner  + + github.com/anacrolix +
- nikk github.com/tsynik + nikk  + + github.com/tsynik +
- dancheskus github.com/dancheskus + dancheskus  + + github.com/dancheskus +
- tw1cker Руслан Пахнев github.com/Nemiroff + tw1cker Руслан Пахнев  + + github.com/Nemiroff +
SpAwN_LMG
diff --git a/web/src/components/Add/AddDialog.jsx b/web/src/components/Add/AddDialog.jsx index aa59b0b..2e0c460 100644 --- a/web/src/components/Add/AddDialog.jsx +++ b/web/src/components/Add/AddDialog.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import Button from '@material-ui/core/Button' import Dialog from '@material-ui/core/Dialog' import { torrentsHost, torrentUploadHost } from 'utils/Hosts' @@ -9,6 +9,11 @@ import useChangeLanguage from 'utils/useChangeLanguage' import { useMediaQuery } from '@material-ui/core' import CircularProgress from '@material-ui/core/CircularProgress' import usePreviousState from 'utils/usePreviousState' +import { useQuery } from 'react-query' +import { getTorrents } from 'utils/Utils' +import parseTorrent from 'parse-torrent' +import { ThemeProvider } from '@material-ui/core/styles' +import { lightTheme } from 'components/App' import { checkImageURL, getMoviePosters, chechTorrentSource, parseTorrentTitle } from './helpers' import { ButtonWrapper, Content, Header } from './style' @@ -23,31 +28,75 @@ export default function AddDialog({ poster: originalPoster, }) { const { t } = useTranslation() + const isEditMode = !!originalHash const [torrentSource, setTorrentSource] = useState(originalHash || '') const [title, setTitle] = useState(originalTitle || '') + const [originalTorrentTitle, setOriginalTorrentTitle] = useState('') + const [parsedTitle, setParsedTitle] = useState('') const [posterUrl, setPosterUrl] = useState(originalPoster || '') const [isPosterUrlCorrect, setIsPosterUrlCorrect] = useState(false) const [isTorrentSourceCorrect, setIsTorrentSourceCorrect] = useState(false) + const [isHashAlreadyExists, setIsHashAlreadyExists] = useState(false) const [posterList, setPosterList] = useState() - const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(false) + const [isUserInteractedWithPoster, setIsUserInteractedWithPoster] = useState(isEditMode) 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 [isEditMode, setIsEditMode] = useState(false) + const [isCustomTitleEnabled, setIsCustomTitleEnabled] = useState(false) + + const { data: torrents } = useQuery('torrents', getTorrents, { + retry: 1, + refetchInterval: 1000, + }) + + useEffect(() => { + const allHashes = torrents.map(({ hash }) => hash) + + parseTorrent.remote(selectedFile || torrentSource, (err, { infoHash } = {}) => { + setIsHashAlreadyExists(allHashes.includes(infoHash)) + }) + }, [selectedFile, torrentSource, torrents]) const fullScreen = useMediaQuery('@media (max-width:930px)') + const updateTitleFromSource = useCallback(() => { + parseTorrentTitle(selectedFile || torrentSource, ({ parsedTitle, originalName }) => { + if (!originalName) return + + setSkipDebounce(true) + setTitle('') + setIsCustomTitleEnabled(false) + setOriginalTorrentTitle(originalName) + setParsedTitle(parsedTitle) + }) + }, [selectedFile, torrentSource]) + + useEffect(() => { + if (!selectedFile && !torrentSource) { + setTitle('') + setOriginalTorrentTitle('') + setParsedTitle('') + setIsCustomTitleEnabled(false) + setPosterList() + removePoster() + setIsUserInteractedWithPoster(false) + } + }, [selectedFile, torrentSource]) + + const removePoster = () => { + setIsPosterUrlCorrect(false) + setPosterUrl('') + } + useEffect(() => { if (originalHash) { - setIsEditMode(true) - checkImageURL(posterUrl).then(correctImage => { correctImage ? setIsPosterUrlCorrect(true) : removePoster() }) } - // This is needed only on mount + // This is needed only on mount. Do not remove line below // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -85,54 +134,51 @@ export default function AddDialog({ const delayedPosterSearch = useMemo(() => debounce(posterSearch, 700), [posterSearch]) - const prevTitleState = usePreviousState(title) const prevTorrentSourceState = usePreviousState(torrentSource) useEffect(() => { - // if torrentSource is updated then we are checking that source is valid and getting title from the source - const torrentSourceChanged = torrentSource !== prevTorrentSourceState - const isCorrectSource = chechTorrentSource(torrentSource) if (!isCorrectSource) return setIsTorrentSourceCorrect(false) setIsTorrentSourceCorrect(true) - if (torrentSourceChanged) { - parseTorrentTitle(selectedFile || torrentSource, newTitle => { - if (!newTitle) return + // if torrentSource is updated then we are getting title from the source + const torrentSourceChanged = torrentSource !== prevTorrentSourceState + if (!torrentSourceChanged) return - setSkipDebounce(true) - setTitle(newTitle) - }) - } - }, [prevTorrentSourceState, selectedFile, torrentSource]) + updateTitleFromSource() + }, [prevTorrentSourceState, selectedFile, torrentSource, updateTitleFromSource]) + + const prevTitleState = usePreviousState(title) useEffect(() => { // if title exists and title was changed then search poster. const titleChanged = title !== prevTitleState - if (!titleChanged) return + if (!titleChanged && !parsedTitle) return if (skipDebounce) { - posterSearch(title, posterSearchLanguage) + posterSearch(title || parsedTitle, posterSearchLanguage) setSkipDebounce(false) + } else if (!title) { + if (parsedTitle) { + posterSearch(parsedTitle, posterSearchLanguage) + } else { + delayedPosterSearch.cancel() + !isUserInteractedWithPoster && removePoster() + } } else { - title === '' ? removePoster() : delayedPosterSearch(title, posterSearchLanguage) + delayedPosterSearch(title, posterSearchLanguage) } - }, [title, prevTitleState, delayedPosterSearch, posterSearch, posterSearchLanguage, skipDebounce]) - - const removePoster = () => { - setIsPosterUrlCorrect(false) - setPosterUrl('') - } - - useEffect(() => { - if (!selectedFile && !torrentSource) { - setTitle('') - setPosterList() - removePoster() - setIsUserInteractedWithPoster(false) - } - }, [selectedFile, torrentSource]) + }, [ + title, + parsedTitle, + prevTitleState, + delayedPosterSearch, + posterSearch, + posterSearchLanguage, + skipDebounce, + isUserInteractedWithPoster, + ]) const handleSave = () => { setIsLoadingButton(true) @@ -142,7 +188,7 @@ export default function AddDialog({ .post(torrentsHost(), { action: 'set', hash: originalHash, - title: title === '' ? originalName : title, + title: title || originalName, poster: posterUrl, }) .finally(handleClose) @@ -163,62 +209,71 @@ export default function AddDialog({ } return ( - -
{t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')}
+ + +
{t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')}
- - {!isEditMode && ( - + {!isEditMode && ( + + )} + + - )} + - - + + - - - - - -
+ + +
+ ) } diff --git a/web/src/components/Add/LeftSideComponent.jsx b/web/src/components/Add/LeftSideComponent.jsx index 11c9df9..a5ceda1 100644 --- a/web/src/components/Add/LeftSideComponent.jsx +++ b/web/src/components/Add/LeftSideComponent.jsx @@ -50,8 +50,8 @@ export default function LeftSideComponent({ onChange={handleTorrentSourceChange} value={torrentSource} margin='dense' - label={t('TorrentSourceLink')} - helperText={t('TorrentSourceOptions')} + label={t('AddDialog.TorrentSourceLink')} + helperText={t('AddDialog.TorrentSourceOptions')} type='text' fullWidth onFocus={() => setIsTorrentSourceActive(true)} @@ -74,11 +74,11 @@ export default function LeftSideComponent({ ) : ( -
{t('AppendFile.Or')}
+
{t('AddDialog.AppendFile.Or')}
-
{t('AppendFile.ClickOrDrag')}
+
{t('AddDialog.AppendFile.ClickOrDrag')}
)} diff --git a/web/src/components/Add/RightSideComponent.jsx b/web/src/components/Add/RightSideComponent.jsx index 75da6f0..e8edf6c 100644 --- a/web/src/components/Add/RightSideComponent.jsx +++ b/web/src/components/Add/RightSideComponent.jsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import { NoImageIcon } from 'icons' -import { TextField } from '@material-ui/core' +import { IconButton, InputAdornment, TextField } from '@material-ui/core' +import { CheckBox as CheckBoxIcon } from '@material-ui/icons' import { ClearPosterButton, @@ -21,7 +22,9 @@ export default function RightSideComponent({ setIsUserInteractedWithPoster, setPosterList, isTorrentSourceCorrect, + isHashAlreadyExists, title, + parsedTitle, posterUrl, isPosterUrlCorrect, posterList, @@ -31,6 +34,11 @@ export default function RightSideComponent({ posterSearch, removePoster, torrentSource, + originalTorrentTitle, + updateTitleFromSource, + isCustomTitleEnabled, + setIsCustomTitleEnabled, + isEditMode, }) { const { t } = useTranslation() @@ -49,13 +57,61 @@ export default function RightSideComponent({ return ( - - + + {originalTorrentTitle ? ( + <> + + setIsCustomTitleEnabled(true)} + onBlur={({ target: { value } }) => !value && setIsCustomTitleEnabled(false)} + value={title} + margin='dense' + label={t('AddDialog.CustomTorrentTitle')} + type='text' + fullWidth + InputProps={{ + endAdornment: ( + + { + setTitle('') + setIsCustomTitleEnabled(!isCustomTitleEnabled) + updateTitleFromSource() + setIsUserInteractedWithPoster(false) + }} + > + + + + ), + }} + /> + + ) : ( + + )} @@ -81,7 +137,9 @@ export default function RightSideComponent({ onClick={() => { const newLanguage = posterSearchLanguage === 'en' ? 'ru' : 'en' setPosterSearchLanguage(newLanguage) - posterSearch(title, newLanguage, { shouldRefreshMainPoster: true }) + posterSearch(isCustomTitleEnabled ? title : originalTorrentTitle ? parsedTitle : title, newLanguage, { + shouldRefreshMainPoster: true, + }) }} showbutton={+isPosterUrlCorrect} color='primary' @@ -108,11 +166,15 @@ export default function RightSideComponent({ ) diff --git a/web/src/components/Add/helpers.js b/web/src/components/Add/helpers.js index 3bb5ebe..f319786 100644 --- a/web/src/components/Add/helpers.js +++ b/web/src/components/Add/helpers.js @@ -32,14 +32,14 @@ export const checkImageURL = async url => { } 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 +export 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) + if (!name || err) return callback({ parsedTitle: null, originalName: null }) const torrentName = ptt.parse(name).title const nameOfFileInsideTorrent = files ? ptt.parse(files[0].name).title : null @@ -50,6 +50,6 @@ export const parseTorrentTitle = (parsingSource, callback) => { newTitle = torrentName.length < nameOfFileInsideTorrent.length ? torrentName : nameOfFileInsideTorrent } - callback(newTitle) + callback({ parsedTitle: newTitle, originalName: name }) }) } diff --git a/web/src/components/Add/style.js b/web/src/components/Add/style.js index b286159..ac7eb26 100644 --- a/web/src/components/Add/style.js +++ b/web/src/components/Add/style.js @@ -6,7 +6,7 @@ export const Header = styled.div` color: rgba(0, 0, 0, 0.87); font-size: 20px; color: #fff; - font-weight: 500; + font-weight: 600; 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; @@ -14,6 +14,7 @@ export const Header = styled.div` export const Content = styled.div` ${({ isEditMode }) => css` + height: 550px; background: linear-gradient(145deg, #e4f6ed, #b5dec9); flex: 1; display: grid; @@ -29,6 +30,10 @@ export const Content = styled.div` @media (max-width: 930px) { grid-template-columns: 1fr; } + + @media (max-width: 500px) { + align-content: start; + } `} ` @@ -38,7 +43,7 @@ export const RightSide = styled.div` export const RightSideContainer = styled.div` ${({ isHidden, notificationMessage, isError }) => css` - height: 455px; + height: 530px; ${notificationMessage && css` @@ -54,7 +59,7 @@ export const RightSideContainer = styled.div` background: ${isError ? '#cda184' : '#84cda7'}; padding: 10px 15px; position: absolute; - top: 50%; + top: 52%; left: 50%; transform: translate(-50%, -50%); border-radius: 5px; @@ -65,6 +70,10 @@ export const RightSideContainer = styled.div` css` display: none; `}; + + @media (max-width: 500px) { + height: 170px; + } `} ` export const LeftSide = styled.div` @@ -88,7 +97,7 @@ export const LeftSideBottomSectionNoFile = styled.div` ${({ isDragActive }) => isDragActive && `border: 4px dashed green`}; justify-items: center; - grid-template-rows: 100px 1fr; + grid-template-rows: 130px 1fr; cursor: pointer; :hover { @@ -104,6 +113,15 @@ export const LeftSideBottomSectionNoFile = styled.div` place-items: center; grid-template-rows: 40% 1fr; } + + @media (max-width: 500px) { + height: 170px; + grid-template-rows: 1fr; + + > div:first-of-type { + display: none; + } + } ` export const LeftSideBottomSectionFileSelected = styled.div` @@ -113,6 +131,10 @@ export const LeftSideBottomSectionFileSelected = styled.div` @media (max-width: 930px) { height: 400px; } + + @media (max-width: 500px) { + height: 170px; + } ` export const TorrentIconWrapper = styled.div` @@ -281,7 +303,7 @@ export const PosterLanguageSwitch = styled.div` display: grid; place-items: center; color: #e1f4eb; - font-weight: 500; + font-weight: 600; cursor: pointer; transition: all 0.3s; diff --git a/web/src/components/App/Sidebar.jsx b/web/src/components/App/Sidebar.jsx index 5434289..1c76c86 100644 --- a/web/src/components/App/Sidebar.jsx +++ b/web/src/components/App/Sidebar.jsx @@ -1,12 +1,10 @@ -import { playlistAllHost } from 'utils/Hosts' import Divider from '@material-ui/core/Divider' import ListItem from '@material-ui/core/ListItem' import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemText from '@material-ui/core/ListItemText' -import { CreditCard as CreditCardIcon, List as ListIcon, Language as LanguageIcon } from '@material-ui/icons' +import { CreditCard as CreditCardIcon } from '@material-ui/icons' import List from '@material-ui/core/List' import { useTranslation } from 'react-i18next' -import useChangeLanguage from 'utils/useChangeLanguage' import AddDialogButton from 'components/Add' import SettingsDialog from 'components/Settings' import RemoveAll from 'components/RemoveAll' @@ -16,7 +14,6 @@ import CloseServer from 'components/CloseServer' import { AppSidebarStyle } from './style' export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - const [currentLang, changeLang] = useChangeLanguage() const { t } = useTranslation() return ( @@ -24,33 +21,20 @@ export default function Sidebar({ isDrawerOpen, setIsDonationDialogOpen }) { - - - - - - - - (currentLang === 'en' ? changeLang('ru') : changeLang('en'))}> - - - - - - - + + setIsDonationDialogOpen(true)}> diff --git a/web/src/components/App/index.jsx b/web/src/components/App/index.jsx index e212c60..665b0e3 100644 --- a/web/src/components/App/index.jsx +++ b/web/src/components/App/index.jsx @@ -1,6 +1,7 @@ -import CssBaseline from '@material-ui/core/CssBaseline' +import useMediaQuery from '@material-ui/core/useMediaQuery' import { createMuiTheme, MuiThemeProvider } from '@material-ui/core' -import { useEffect, useState } from 'react' +import CssBaseline from '@material-ui/core/CssBaseline' +import { useEffect, useMemo, useState } from 'react' import Typography from '@material-ui/core/Typography' import IconButton from '@material-ui/core/IconButton' import { Menu as MenuIcon, Close as CloseIcon } from '@material-ui/icons' @@ -10,19 +11,51 @@ import axios from 'axios' import TorrentList from 'components/TorrentList' import DonateSnackbar from 'components/Donate' import DonateDialog from 'components/Donate/DonateDialog' +import useChangeLanguage from 'utils/useChangeLanguage' +import { ThemeProvider } from '@material-ui/core/styles' -import { AppWrapper, AppHeader } from './style' +import { AppWrapper, AppHeader, LanguageSwitch } from './style' import Sidebar from './Sidebar' -const baseTheme = createMuiTheme({ - overrides: { MuiCssBaseline: { '@global': { html: { WebkitFontSmoothing: 'auto' } } } }, - palette: { primary: { main: '#00a572' }, secondary: { main: '#ffa724' }, tonalOffset: 0.2 }, +// https://material-ui.com/ru/customization/default-theme/ +export const darkTheme = createMuiTheme({ + palette: { + type: 'dark', + primary: { main: '#00a572' }, + background: { paper: '#575757' }, + }, + typography: { fontFamily: 'Open Sans, sans-serif' }, +}) +export const lightTheme = createMuiTheme({ + palette: { + type: 'light', + primary: { main: '#00a572' }, + background: { paper: '#cbe8d9' }, + }, + typography: { fontFamily: 'Open Sans, sans-serif' }, }) export default function App() { + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') const [isDrawerOpen, setIsDrawerOpen] = useState(false) const [isDonationDialogOpen, setIsDonationDialogOpen] = useState(false) const [torrServerVersion, setTorrServerVersion] = useState('') + // https://material-ui.com/ru/customization/palette/ + const baseTheme = useMemo( + () => + createMuiTheme({ + overrides: { MuiCssBaseline: { '@global': { html: { WebkitFontSmoothing: 'auto' } } } }, + palette: { + type: prefersDarkMode ? 'dark' : 'light', + primary: { main: '#00a572' }, + secondary: { main: '#ffa724' }, + tonalOffset: 0.2, + }, + typography: { fontFamily: 'Open Sans, sans-serif' }, + }), + [prefersDarkMode], + ) + const [currentLang, changeLang] = useChangeLanguage() useEffect(() => { axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data)) @@ -48,13 +81,20 @@ export default function App() { TorrServer {torrServerVersion} + +
+ (currentLang === 'en' ? changeLang('ru') : changeLang('en'))}> + {currentLang === 'en' ? 'ru' : 'en'} + +
- - - + + + - - {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} + + {isDonationDialogOpen && setIsDonationDialogOpen(false)} />} + {!JSON.parse(localStorage.getItem('snackbarIsClosed')) && } diff --git a/web/src/components/App/style.js b/web/src/components/App/style.js index 436b758..71e9e8a 100644 --- a/web/src/components/App/style.js +++ b/web/src/components/App/style.js @@ -19,10 +19,12 @@ export const CenteredGrid = styled.div` export const AppHeader = styled.div` background: #00a572; - color: rgba(0, 0, 0, 0.87); + color: #eee; grid-area: head; - display: flex; + display: grid; + grid-auto-flow: column; align-items: center; + grid-template-columns: repeat(2, max-content) 1fr; 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: 0 24px; z-index: 3; @@ -35,7 +37,8 @@ export const AppSidebarStyle = styled.div` overflow-x: hidden; transition: width 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms; border-right: 1px solid rgba(0, 0, 0, 0.12); - background: #eee; + background: #575757; + color: #eee; white-space: nowrap; `} ` @@ -63,3 +66,25 @@ export const TorrentListWrapper = styled.div` grid-template-columns: 1fr; } ` + +export const LanguageSwitch = styled.div` + cursor: pointer; + border-radius: 50%; + background: #56b887; + height: 35px; + width: 35px; + transition: all 0.2s; + font-weight: 600; + display: grid; + place-items: center; + color: #eee; + + :hover { + background: #7ec9a3; + } + + @media (max-width: 700px) { + height: 28px; + width: 28px; + } +` diff --git a/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent b/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent deleted file mode 100644 index 099c998..0000000 Binary files a/web/src/components/DialogTorrentDetailsContent/Table/[kinozal.tv]id1846470.torrent and /dev/null differ diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js index 82c4463..4621717 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/snakeSettings.js @@ -4,9 +4,9 @@ export const pieceSizeForMiniMap = 23 export const gapBetweenPieces = 3 export const miniCacheMaxHeight = 340 -export const defaultBorderColor = '#eef2f4' +export const defaultBorderColor = '#dbf2e8' export const defaultBackgroundColor = '#fff' export const completeColor = '#00a572' export const progressColor = '#ffa724' export const activeColor = '#000' -export const rangeColor = '#9a9aff' +export const rangeColor = '#ffa724' diff --git a/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js index 532a6d3..3887f5d 100644 --- a/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js +++ b/web/src/components/DialogTorrentDetailsContent/TorrentCache/style.js @@ -62,5 +62,5 @@ export const SnakeWrapper = styled.div` export const PercentagePiece = styled.div` background: ${completeColor}; - height: ${({ percentage }) => (percentage / 100) * 12}px; + height: ${({ percentage }) => percentage}%; ` diff --git a/web/src/components/DialogTorrentDetailsContent/index.jsx b/web/src/components/DialogTorrentDetailsContent/index.jsx index f111305..0138ddd 100644 --- a/web/src/components/DialogTorrentDetailsContent/index.jsx +++ b/web/src/components/DialogTorrentDetailsContent/index.jsx @@ -1,5 +1,5 @@ import { NoImageIcon } from 'icons' -import { humanizeSize, shortenText } from 'utils/Utils' +import { humanizeSize, removeRedundantCharacters } from 'utils/Utils' import { useEffect, useState } from 'react' import { Button, ButtonGroup } from '@material-ui/core' import ptt from 'parse-torrent-title' @@ -102,18 +102,27 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) { const bufferSize = settings?.PreloadBuffer ? Capacity : 33554432 // Default is 32mb if PreloadBuffer is false const getParsedTitle = () => { - const newNameStrings = [] + const newNameStringArr = [] const torrentParsedName = name && ptt.parse(name) if (title !== name) { - newNameStrings.push(title) - } else if (torrentParsedName?.title) newNameStrings.push(torrentParsedName?.title) + newNameStringArr.push(removeRedundantCharacters(title)) + } else if (torrentParsedName?.title) newNameStringArr.push(removeRedundantCharacters(torrentParsedName?.title)) - if (torrentParsedName?.year) newNameStrings.push(torrentParsedName?.year) - if (torrentParsedName?.resolution) newNameStrings.push(torrentParsedName?.resolution) + // These 2 checks are needed to get year and resolution from torrent name if title does not have this info + if (torrentParsedName?.year && !newNameStringArr[0].includes(torrentParsedName?.year)) + newNameStringArr.push(torrentParsedName?.year) + if (torrentParsedName?.resolution && !newNameStringArr[0].includes(torrentParsedName?.resolution)) + newNameStringArr.push(torrentParsedName?.resolution) - return newNameStrings.join('. ') + const newNameString = newNameStringArr.join('. ') + + // removeRedundantCharacters is returning ".." if it was "..." + const lastDotShouldBeAdded = + newNameString[newNameString.length - 1] === '.' && newNameString[newNameString.length - 2] === '.' + + return lastDotShouldBeAdded ? `${newNameString}.` : newNameString } return ( @@ -145,12 +154,19 @@ export default function DialogTorrentDetailsContent({ closeDialog, torrent }) {
{title && name !== title ? ( - <> - {shortenText(getParsedTitle(), 55)} - {shortenText(ptt.parse(name).title, 110)} - + getParsedTitle().length > 90 ? ( + <> + {ptt.parse(name).title} + {getParsedTitle()} + + ) : ( + <> + {getParsedTitle()} + {ptt.parse(name).title} + + ) ) : ( - {shortenText(getParsedTitle(), 55)} + {getParsedTitle()} )} diff --git a/web/src/components/DialogTorrentDetailsContent/style.js b/web/src/components/DialogTorrentDetailsContent/style.js index f134e01..4884967 100644 --- a/web/src/components/DialogTorrentDetailsContent/style.js +++ b/web/src/components/DialogTorrentDetailsContent/style.js @@ -113,7 +113,7 @@ export const SectionTitle = styled.div` ${({ mb }) => css` ${mb && `margin-bottom: ${mb}px`}; font-size: 35px; - font-weight: 200; + font-weight: 300; line-height: 1; word-break: break-word; @@ -187,7 +187,7 @@ export const WidgetFieldTitle = styled.div` text-transform: uppercase; font-size: 11px; margin-bottom: 2px; - font-weight: 500; + font-weight: 600; ` export const WidgetFieldIcon = styled.div` diff --git a/web/src/components/Donate/DonateDialog.jsx b/web/src/components/Donate/DonateDialog.jsx index 4c5c98c..ccd2d28 100644 --- a/web/src/components/Donate/DonateDialog.jsx +++ b/web/src/components/Donate/DonateDialog.jsx @@ -15,7 +15,7 @@ export default function DonateDialog({ onClose }) { const { t } = useTranslation() return ( - + {t('Donate')} diff --git a/web/src/components/TorrentCard/index.jsx b/web/src/components/TorrentCard/index.jsx index dc88def..bab6525 100644 --- a/web/src/components/TorrentCard/index.jsx +++ b/web/src/components/TorrentCard/index.jsx @@ -1,4 +1,3 @@ -import 'fontsource-roboto' import { forwardRef, memo, useState } from 'react' import { UnfoldMore as UnfoldMoreIcon, @@ -6,7 +5,7 @@ import { Close as CloseIcon, Delete as DeleteIcon, } from '@material-ui/icons' -import { getPeerString, humanizeSize, shortenText } from 'utils/Utils' +import { getPeerString, humanizeSize, removeRedundantCharacters } from 'utils/Utils' import { torrentsHost } from 'utils/Hosts' import { NoImageIcon } from 'icons' import DialogTorrentDetailsContent from 'components/DialogTorrentDetailsContent' @@ -40,7 +39,21 @@ const Torrent = ({ torrent }) => { const dropTorrent = () => axios.post(torrentsHost(), { action: 'drop', hash }) const deleteTorrent = () => axios.post(torrentsHost(), { action: 'rem', hash }) - const parsedTitle = (title || name) && ptt.parse(title || name).title + const getParsedTitle = () => { + const parse = key => ptt.parse(title || '')?.[key] || ptt.parse(name || '')?.[key] + + const titleStrings = [] + + let parsedTitle = removeRedundantCharacters(parse('title')) + const parsedYear = parse('year') + const parsedResolution = parse('resolution') + if (parsedTitle) titleStrings.push(parsedTitle) + if (parsedYear) titleStrings.push(`(${parsedYear})`) + if (parsedResolution) titleStrings.push(`[${parsedResolution}]`) + parsedTitle = titleStrings.join(' ') + return { parsedTitle } + } + const { parsedTitle } = getParsedTitle() const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const handleClickOpenEditDialog = () => setIsEditDialogOpen(true) @@ -78,7 +91,7 @@ const Torrent = ({ torrent }) => {
{t('Name')}
-
{shortenText(parsedTitle, 100)}
+
{parsedTitle}
diff --git a/web/src/components/TorrentCard/style.js b/web/src/components/TorrentCard/style.js index eb696bb..1e8e874 100644 --- a/web/src/components/TorrentCard/style.js +++ b/web/src/components/TorrentCard/style.js @@ -87,10 +87,6 @@ export const TorrentCardDescription = styled.div` gap: 3px; } - @media (max-width: 770px) { - grid-template-rows: 56% 1fr; - } - .description-title-wrapper { display: flex; flex-direction: column; @@ -99,7 +95,7 @@ export const TorrentCardDescription = styled.div` .description-section-name { text-transform: uppercase; font-size: 10px; - font-weight: 500; + font-weight: 600; letter-spacing: 0.4px; color: #216e47; @@ -170,7 +166,6 @@ export const StyledButton = styled.button` background: #268757; color: #fff; font-size: 0.9rem; - font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; letter-spacing: 0.009em; padding: 0 12px; svg { diff --git a/web/src/components/TorrentList.jsx b/web/src/components/TorrentList.jsx index 62887aa..b5fd512 100644 --- a/web/src/components/TorrentList.jsx +++ b/web/src/components/TorrentList.jsx @@ -1,21 +1,11 @@ import { useState } from 'react' import { Typography } from '@material-ui/core' -import { torrentsHost } from 'utils/Hosts' import TorrentCard from 'components/TorrentCard' -import axios from 'axios' import CircularProgress from '@material-ui/core/CircularProgress' import { TorrentListWrapper, CenteredGrid } from 'components/App/style' import { useTranslation } from 'react-i18next' import { useQuery } from 'react-query' - -const getTorrents = async () => { - try { - const { data } = await axios.post(torrentsHost(), { action: 'list' }) - return data - } catch (error) { - throw new Error(null) - } -} +import { getTorrents } from 'utils/Utils' export default function TorrentList() { const { t } = useTranslation() diff --git a/web/src/index.css b/web/src/index.css index ec2585e..d45520b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,13 +1,20 @@ -body { +*, +*::before, +*::after { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + padding: 0; + box-sizing: inherit; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; +body { + font-family: "Open Sans", sans-serif; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.1px; } + +button { + font-family: "Open Sans", sans-serif; + letter-spacing: -0.1px; +} \ No newline at end of file diff --git a/web/src/locales/en/translation.json b/web/src/locales/en/translation.json index e156036..7db9381 100644 --- a/web/src/locales/en/translation.json +++ b/web/src/locales/en/translation.json @@ -2,21 +2,29 @@ "About": "About", "Actions": "Actions", "Add": "Add", + "AddDialog": { + "AddPosterLinkInput": "Poster link", + "AddTorrentSourceNotification": "First add your torrent source", + "AppendFile": { + "Or": "OR", + "ClickOrDrag": "CLICK / DRAG & DROP (.torrent)" + }, + "CustomTorrentTitle": "Custom title (optional)", + "HashExists": "This torrent is already in database", + "OriginalTorrentTitle": "Original torrent title", + "TitleBlank": "Title (blank for orig. torrent title)", + "TorrentSourceLink": "Torrent source link", + "TorrentSourceOptions": "magnet / hash / .torrent file link", + "WrongTorrentSource": "Wrong torrent source" + }, "AddFromLink": "Add from Link", "AddNewTorrent": "Add new torrent", - "AddPosterLinkInput": "Poster link", "AddRetrackers": "Add retrackers", - "AddTorrentSourceNotification": "First add your torrent source", - "AppendFile": { - "Or": "OR", - "ClickOrDrag": "CLICK / DRAG & DROP (.torrent)" - }, "Buffer": "Preload Buffer / Cache", "BufferNote": "Enable “Preload Buffer” in settings to see cache loading progress", "Cache": "Cache", "CacheSize": "Cache Size (Megabytes)", "Cancel": "Cancel", - "ChooseLanguage": "Russian", "Clear": "Clear", "Close": "Close", "CloseServer?": "Do you want to turn off server?", @@ -63,7 +71,6 @@ "PEX": "PEX (Peer Exchange)", "PiecesCount": "Pieces count", "PiecesLength": "Pieces length", - "PlaylistAll": "Playlist All", "Preload": "Preload", "PreloadBuffer": "Preload Buffer", "ReaderReadAHead": "Reader Read Ahead (5-100%)", @@ -86,7 +93,6 @@ "Support": "Support", "TCP": "TCP (Transmission Control Protocol)", "ThanksToEveryone": "Thanks to everyone who tested and helped.", - "Title": "Title", "TorrentAdded": "Added", "TorrentClosed": "Сlosed", "TorrentContent": "Torrent Content", @@ -96,8 +102,6 @@ "TorrentInDb": "In DB", "TorrentPreload": "Preload", "TorrentSize": "Torrent size", - "TorrentSourceLink": "Torrent source link", - "TorrentSourceOptions": "magnet / hash / .torrent file link", "TorrentsSavePath": "Torrents Save Path", "TorrentState": "Torrent State", "TorrentStatus": "Torrent Status", @@ -111,6 +115,5 @@ "UseDisk": "Use Disk for Cache", "UseDiskDesc": "Better use external media on flash-based devices", "UTP": "μTP (Micro Transport Protocol)", - "Viewed": "Viewed", - "WrongTorrentSource": "Wrong torrent source" + "Viewed": "Viewed" } \ No newline at end of file diff --git a/web/src/locales/ru/translation.json b/web/src/locales/ru/translation.json index 25218f9..dff1009 100644 --- a/web/src/locales/ru/translation.json +++ b/web/src/locales/ru/translation.json @@ -2,21 +2,29 @@ "About": "О сервере", "Actions": "Действия", "Add": "Добавить", + "AddDialog": { + "AddPosterLinkInput": "Ссылка на постер", + "AddTorrentSourceNotification": "Сначала добавьте torrent-источник", + "AppendFile": { + "Or": "ИЛИ", + "ClickOrDrag": "НАЖМИТЕ / ПЕРЕТАЩИТЕ ФАЙЛ (.torrent)" + }, + "CustomTorrentTitle": "Cвое имя (не обязательно)", + "HashExists": "Этот торрент уже есть в базе данных", + "OriginalTorrentTitle": "Оригинальное имя торрента", + "TitleBlank": "Имя (пустое - ориг. имя торрента)", + "TorrentSourceLink": "Ссылка на источник торрента", + "TorrentSourceOptions": "magnet-ссылка / хеш / ссылка на .torrent файл", + "WrongTorrentSource": "Неправильный torrent-источник" + }, "AddFromLink": "Добавить", "AddNewTorrent": "Добавить новый торрент", - "AddPosterLinkInput": "Ссылка на постер", "AddRetrackers": "Добавлять", - "AddTorrentSourceNotification": "Сначала добавьте torrent-источник", - "AppendFile": { - "Or": "ИЛИ", - "ClickOrDrag": "НАЖМИТЕ / ПЕРЕТАЩИТЕ ФАЙЛ (.torrent)" - }, "Buffer": "Предзагрузка / Кеш", "BufferNote": "Включите «Наполнять кеш перед началом воспроизведения» в настройках для показа заполнения кеша", "Cache": "Кеш", "CacheSize": "Размер кеша (Мегабайты)", "Cancel": "Отмена", - "ChooseLanguage": "Английский", "Clear": "Очистить", "Close": "Закрыть", "CloseServer?": "Хотите выключить сервер?", @@ -58,12 +66,11 @@ "Offline": "Сервер не доступен", "OK": "OK", "OpenLink": "Открыть", - "Peers": "Подкл./Пиры", + "Peers": "Пиры", "PeersListenPort": "Порт для входящих подключений", "PEX": "PEX (Peer Exchange)", "PiecesCount": "Кол-во блоков", "PiecesLength": "Размер блока", - "PlaylistAll": "Плейлист всех", "Preload": "Предзагр.", "PreloadBuffer": "Наполнять кеш перед началом воспроизведения", "ReaderReadAHead": "Кеш предзагрузки (5-100%, рек. 95%)", @@ -86,7 +93,6 @@ "Support": "Поддержать", "TCP": "TCP (Transmission Control Protocol)", "ThanksToEveryone": "Спасибо всем, кто тестировал и помогал!", - "Title": "Название", "TorrentAdded": "Добавлен", "TorrentClosed": "Закрыт", "TorrentContent": "Содержимое торрента", @@ -96,8 +102,6 @@ "TorrentInDb": "Торрент в БД", "TorrentPreload": "Предзагрузка", "TorrentSize": "Размер торрента", - "TorrentSourceLink": "Ссылка на источник торрента", - "TorrentSourceOptions": "magnet-ссылка / хеш / ссылка на .torrent файл", "TorrentsSavePath": "Путь хранения кеша", "TorrentState": "Данные торрента", "TorrentStatus": "Состояние торрента", @@ -111,6 +115,5 @@ "UseDisk": "Использовать диск для кеша", "UseDiskDesc": "Рекомендуется использовать внешние носители на устройствах с flash-памятью", "UTP": "μTP (Micro Transport Protocol)", - "Viewed": "Просм.", - "WrongTorrentSource": "Неправильный torrent-источник" + "Viewed": "Просм." } \ No newline at end of file diff --git a/web/src/utils/Hosts.js b/web/src/utils/Hosts.js index cbc05d9..6e1cc7d 100644 --- a/web/src/utils/Hosts.js +++ b/web/src/utils/Hosts.js @@ -10,7 +10,6 @@ export const settingsHost = () => `${torrserverHost}/settings` export const streamHost = () => `${torrserverHost}/stream` export const shutdownHost = () => `${torrserverHost}/shutdown` export const echoHost = () => `${torrserverHost}/echo` -export const playlistAllHost = () => `${torrserverHost}/playlistall/all.m3u` export const playlistTorrHost = () => `${torrserverHost}/stream` export const getTorrServerHost = () => torrserverHost diff --git a/web/src/utils/Utils.js b/web/src/utils/Utils.js index 06289f7..aca184e 100644 --- a/web/src/utils/Utils.js +++ b/web/src/utils/Utils.js @@ -1,7 +1,11 @@ +import axios from 'axios' + +import { torrentsHost } from './Hosts' + export function humanizeSize(size) { if (!size) return '' const i = Math.floor(Math.log(size) / Math.log(1024)) - return `${(size / Math.pow(1024, i)).toFixed(2) * 1} ${['B', 'kB', 'MB', 'GB', 'TB'][i]}` + return `${(size / Math.pow(1024, i)).toFixed(2) * 1} ${['B', 'KB', 'MB', 'GB', 'TB'][i]}` } export function getPeerString(torrent) { @@ -10,4 +14,43 @@ export function getPeerString(torrent) { } export const shortenText = (text, sympolAmount) => - text ? text.slice(0, sympolAmount) + (text.length > sympolAmount ? '...' : '') : '' + text ? text.slice(0, sympolAmount) + (text.length > sympolAmount ? '…' : '') : '' + +export const removeRedundantCharacters = string => { + let newString = string + const brackets = [ + ['(', ')'], + ['[', ']'], + ['{', '}'], + ] + + brackets.forEach(el => { + const leftBracketRegexFormula = `\\${el[0]}` + const leftBracketRegex = new RegExp(leftBracketRegexFormula, 'g') + const leftBracketAmount = [...newString.matchAll(leftBracketRegex)].length + const rightBracketRegexFormula = `\\${el[1]}` + const rightBracketRegex = new RegExp(rightBracketRegexFormula, 'g') + const rightBracketAmount = [...newString.matchAll(rightBracketRegex)].length + + if (leftBracketAmount !== rightBracketAmount) { + const removeFormula = `(\\${el[0]})(?!.*\\1).*` + const removeRegex = new RegExp(removeFormula, 'g') + newString = newString.replace(removeRegex, '') + } + }) + + const hasThreeDotsAtTheEnd = !!newString.match(/\.{3}$/g) + + const trimmedString = newString.replace(/[\\.| ]+$/g, '').trim() + + return hasThreeDotsAtTheEnd ? `${trimmedString}..` : trimmedString +} + +export const getTorrents = async () => { + try { + const { data } = await axios.post(torrentsHost(), { action: 'list' }) + return data + } catch (error) { + throw new Error(null) + } +} diff --git a/web/yarn.lock b/web/yarn.lock index 34ff6b6..3d65690 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -5808,11 +5808,6 @@ follow-redirects@^1.0.0, follow-redirects@^1.10.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== -fontsource-roboto@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fontsource-roboto/-/fontsource-roboto-4.0.0.tgz#35eacd4fb8d90199053c0eec9b34a57fb79cd820" - integrity sha512-zD6L8nvdWRcwSgp4ojxFchG+MPj8kXXQKDEAH9bfhbxy+lkpvpC1WgAK0lCa4dwobv+hvAe0uyHaawcgH7WH/g== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"