Merge pull request #174 from YouROK/web-updates

Web updates
This commit is contained in:
YouROK
2022-06-27 21:42:39 +03:00
committed by GitHub
78 changed files with 692 additions and 146 deletions

View File

@@ -12,6 +12,7 @@
"endOfLine": "crlf" "endOfLine": "crlf"
}], }],
"import/no-anonymous-default-export": 0, // Allow "export default" "import/no-anonymous-default-export": 0, // Allow "export default"
"import/prefer-default-export": 0,
"import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}], "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}],
"react/jsx-one-expression-per-line": 0, "react/jsx-one-expression-per-line": 0,
"import/order": ["warn", { "import/order": ["warn", {

View File

@@ -15,4 +15,7 @@
> Prettier will fix the code every time the code is saved > Prettier will fix the code every time the code is saved
- `yarn lint` - to find all linting problems - `yarn lint` - to find all linting problems
- `yarn fix` - to fix code - `yarn fix` - to fix code
### How images were generated
`npx pwa-asset-generator public/logo.png public -m public/site.webmanifest -p "calc(50vh - 25%) calc(50vw - 25%)" -b "linear-gradient(135deg, rgb(50,54,55), rgb(84,90,94))" -q 100 -i public/index.html -f`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

BIN
web/public/favicon-196.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,44 +1,66 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="utf-8">
<meta charset="utf-8" /> <link rel="manifest" href="/site.webmanifest">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <meta name="msapplication-TileColor" content="#00a572">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' >
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <meta name="theme-color" content="#ffffff">
<link rel="manifest" href="/site.webmanifest"> <link rel="preconnect" href="https://fonts.gstatic.com">
<meta name="msapplication-TileColor" content="#00a572"> <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&amp;display=swap" rel="stylesheet">
<meta name="theme-color" content="#ffffff"> <meta name="viewport" content="width=device-width, shrink-to-fit=no, viewport-fit=cover, user-scalable=no">
<meta name="description" content="TorrServer - torrent to http stream">
<link rel="preconnect" href="https://fonts.gstatic.com"> <title>TorrServer MatriX</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600&display=swap" rel="stylesheet"> <link rel="icon" type="image/png" sizes="196x196" href="favicon-196.png">
<meta name="viewport" content="width=device-width, shrink-to-fit=no"> <link rel="apple-touch-icon" href="apple-icon-180.png">
<meta name="description" content="TorrServer - torrent to http stream" /> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-startup-image" href="apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<title>TorrServer MatriX</title> <link rel="apple-touch-startup-image" href="apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
</head> <link rel="apple-touch-startup-image" href="apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<body> <link rel="apple-touch-startup-image" href="apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<noscript>You need to enable JavaScript to run this app.</noscript> <link rel="apple-touch-startup-image" href="apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<div id="root"></div> <link rel="apple-touch-startup-image" href="apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<script src='https://cdn.lordicon.com/libs/frhvbuzj/lord-icon-2.0.2.js'></script> <link rel="apple-touch-startup-image" href="apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-app.js"></script> <link rel="apple-touch-startup-image" href="apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-analytics.js"></script> <link rel="apple-touch-startup-image" href="apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<script> <link rel="apple-touch-startup-image" href="apple-splash-2778-1284.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
const firebaseConfig = { <link rel="apple-touch-startup-image" href="apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
apiKey: "AIzaSyDivIsadtzAmp3SIY4yArNcFugUmr63rvo", <link rel="apple-touch-startup-image" href="apple-splash-2532-1170.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
authDomain: "torrserve.firebaseapp.com", <link rel="apple-touch-startup-image" href="apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
databaseURL: "https://torrserve.firebaseio.com", <link rel="apple-touch-startup-image" href="apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
projectId: "torrserve", <link rel="apple-touch-startup-image" href="apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
storageBucket: "torrserve.appspot.com", <link rel="apple-touch-startup-image" href="apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
messagingSenderId: "400168070412", <link rel="apple-touch-startup-image" href="apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
appId: "1:400168070412:web:82c8e43dd7fc8f807aed29", <link rel="apple-touch-startup-image" href="apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
measurementId: "G-T4RC2BFRSF" <link rel="apple-touch-startup-image" href="apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
}; <link rel="apple-touch-startup-image" href="apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
firebase.initializeApp(firebaseConfig); <link rel="apple-touch-startup-image" href="apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
firebase.analytics(); <link rel="apple-touch-startup-image" href="apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
</script> <link rel="apple-touch-startup-image" href="apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
</body> <link rel="apple-touch-startup-image" href="apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
</head>
</html>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="https://cdn.lordicon.com/libs/frhvbuzj/lord-icon-2.0.2.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.1.2/firebase-analytics.js"></script>
<script>
const firebaseConfig = {
apiKey: "AIzaSyDivIsadtzAmp3SIY4yArNcFugUmr63rvo",
authDomain: "torrserve.firebaseapp.com",
databaseURL: "https://torrserve.firebaseio.com",
projectId: "torrserve",
storageBucket: "torrserve.appspot.com",
messagingSenderId: "400168070412",
appId: "1:400168070412:web:82c8e43dd7fc8f807aed29",
measurementId: "G-T4RC2BFRSF"
};
firebase.initializeApp(firebaseConfig);
firebase.analytics();
</script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,19 +1,33 @@
{ {
"name": "", "name": "TorrServer",
"short_name": "", "short_name": "TorrServer",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "manifest-icon-192.maskable.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png",
}, "purpose": "any"
{ },
"src": "/android-chrome-512x512.png", {
"sizes": "512x512", "src": "manifest-icon-192.maskable.png",
"type": "image/png" "sizes": "192x192",
} "type": "image/png",
], "purpose": "maskable"
"theme_color": "#ffffff", },
"background_color": "#ffffff", {
"display": "standalone" "src": "manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
} }

View File

@@ -1,14 +1,15 @@
import axios from 'axios' import axios from 'axios'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import InfoIcon from '@material-ui/icons/Info' import InfoIcon from '@material-ui/icons/Info'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText' import ListItemText from '@material-ui/core/ListItemText'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMediaQuery } from '@material-ui/core' import { useMediaQuery } from '@material-ui/core'
import { echoHost } from 'utils/Hosts' import { echoHost } from 'utils/Hosts'
import { StyledDialog, StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles'
import { isStandaloneApp } from 'utils/Utils'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
import LinkComponent from './LinkComponent' import LinkComponent from './LinkComponent'
import { DialogWrapper, HeaderSection, ThanksSection, Section, FooterSection } from './style' import { DialogWrapper, HeaderSection, ThanksSection, Section, FooterSection } from './style'
@@ -22,27 +23,41 @@ export default function AboutDialog() {
axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data)) axios.get(echoHost()).then(({ data }) => setTorrServerVersion(data))
}, []) }, [])
const onClose = () => setOpen(false)
const ref = useOnStandaloneAppOutsideClick(onClose)
return ( return (
<> <>
<ListItem button key='Settings' onClick={() => setOpen(true)}> <StyledMenuButtonWrapper button key='Settings' onClick={() => setOpen(true)}>
<ListItemIcon> {isStandaloneApp ? (
<InfoIcon /> <>
</ListItemIcon> <InfoIcon />
<ListItemText primary={t('About')} /> <div>{t('Details')}</div>
</ListItem> </>
) : (
<>
<ListItemIcon>
<InfoIcon />
</ListItemIcon>
<Dialog <ListItemText primary={t('About')} />
</>
)}
</StyledMenuButtonWrapper>
<StyledDialog
open={open} open={open}
onClose={() => setOpen(false)} onClose={onClose}
aria-labelledby='form-dialog-title' aria-labelledby='form-dialog-title'
fullScreen={fullScreen} fullScreen={fullScreen}
maxWidth='xl' maxWidth='xl'
ref={ref}
> >
<DialogWrapper> <DialogWrapper>
<HeaderSection> <HeaderSection>
<div>{t('About')}</div> <div>{t('About')}</div>
{torrServerVersion} {torrServerVersion}
<img src='/apple-touch-icon.png' alt='ts-icon' /> <img src='/apple-icon-180.png' alt='ts-icon' />
</HeaderSection> </HeaderSection>
<div style={{ overflow: 'auto' }}> <div style={{ overflow: 'auto' }}>
@@ -72,12 +87,12 @@ export default function AboutDialog() {
</div> </div>
<FooterSection> <FooterSection>
<Button onClick={() => setOpen(false)} color='primary' variant='contained'> <Button onClick={onClose} color='primary' variant='contained'>
{t('Close')} {t('Close')}
</Button> </Button>
</FooterSection> </FooterSection>
</DialogWrapper> </DialogWrapper>
</Dialog> </StyledDialog>
</> </>
) )
} }

View File

@@ -1,4 +1,5 @@
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { standaloneMedia } from 'style/standaloneMedia'
export const DialogWrapper = styled.div` export const DialogWrapper = styled.div`
height: 100%; height: 100%;
@@ -26,6 +27,10 @@ export const HeaderSection = styled.section`
width: 60px; width: 60px;
} }
} }
${standaloneMedia(css`
padding-top: 30px;
`)}
` `
export const ThanksSection = styled.section` export const ThanksSection = styled.section`

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import { torrentsHost, torrentUploadHost } from 'utils/Hosts' import { torrentsHost, torrentUploadHost } from 'utils/Hosts'
import axios from 'axios' import axios from 'axios'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -12,7 +11,9 @@ import usePreviousState from 'utils/usePreviousState'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { getTorrents } from 'utils/Utils' import { getTorrents } from 'utils/Utils'
import parseTorrent from 'parse-torrent' import parseTorrent from 'parse-torrent'
import { ButtonWrapper, Header } from 'style/DialogStyles' import { ButtonWrapper } from 'style/DialogStyles'
import { StyledDialog, StyledHeader } from 'style/CustomMaterialUiStyles'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
import { checkImageURL, getMoviePosters, checkTorrentSource, parseTorrentTitle } from './helpers' import { checkImageURL, getMoviePosters, checkTorrentSource, parseTorrentTitle } from './helpers'
import { Content } from './style' import { Content } from './style'
@@ -46,6 +47,8 @@ export default function AddDialog({
const [isCustomTitleEnabled, setIsCustomTitleEnabled] = useState(false) const [isCustomTitleEnabled, setIsCustomTitleEnabled] = useState(false)
const [currentSourceHash, setCurrentSourceHash] = useState() const [currentSourceHash, setCurrentSourceHash] = useState()
const ref = useOnStandaloneAppOutsideClick(handleClose)
const { data: torrents } = useQuery('torrents', getTorrents, { retry: 1, refetchInterval: 1000 }) const { data: torrents } = useQuery('torrents', getTorrents, { retry: 1, refetchInterval: 1000 })
useEffect(() => { useEffect(() => {
@@ -223,8 +226,8 @@ export default function AddDialog({
} }
return ( return (
<Dialog open onClose={handleClose} fullScreen={fullScreen} fullWidth maxWidth='md'> <StyledDialog open onClose={handleClose} fullScreen={fullScreen} fullWidth maxWidth='md' ref={ref}>
<Header>{t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')}</Header> <StyledHeader>{t(isEditMode ? 'EditTorrent' : 'AddNewTorrent')}</StyledHeader>
<Content isEditMode={isEditMode}> <Content isEditMode={isEditMode}>
{!isEditMode && ( {!isEditMode && (
@@ -279,6 +282,6 @@ export default function AddDialog({
{isSaving ? <CircularProgress style={{ color: 'white' }} size={20} /> : t(isEditMode ? 'Save' : 'Add')} {isSaving ? <CircularProgress style={{ color: 'white' }} size={20} /> : t(isEditMode ? 'Save' : 'Add')}
</Button> </Button>
</ButtonWrapper> </ButtonWrapper>
</Dialog> </StyledDialog>
) )
} }

View File

@@ -2,10 +2,12 @@ import { useState } from 'react'
import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemIcon from '@material-ui/core/ListItemIcon'
import LibraryAddIcon from '@material-ui/icons/LibraryAdd' import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
import ListItemText from '@material-ui/core/ListItemText' import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles'
import { isStandaloneApp } from 'utils/Utils'
import AddDialog from './AddDialog' import AddDialog from './AddDialog'
import { StyledPWAAddButton } from './style'
export default function AddDialogButton({ isOffline, isLoading }) { export default function AddDialogButton({ isOffline, isLoading }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -15,12 +17,19 @@ export default function AddDialogButton({ isOffline, isLoading }) {
return ( return (
<div> <div>
<ListItem disabled={isOffline || isLoading} button onClick={handleClickOpen}> <StyledMenuButtonWrapper disabled={isOffline || isLoading} button onClick={handleClickOpen}>
<ListItemIcon> {isStandaloneApp ? (
<LibraryAddIcon /> <StyledPWAAddButton />
</ListItemIcon> ) : (
<ListItemText primary={t('AddFromLink')} /> <>
</ListItem> <ListItemIcon>
<LibraryAddIcon />
</ListItemIcon>
<ListItemText primary={t('AddFromLink')} />
</>
)}
</StyledMenuButtonWrapper>
{isDialogOpen && <AddDialog handleClose={handleClose} />} {isDialogOpen && <AddDialog handleClose={handleClose} />}
</div> </div>

View File

@@ -95,6 +95,7 @@ export const LeftSideBottomSectionNoFile = styled.div`
${LeftSideBottomSectionBasicStyles} ${LeftSideBottomSectionBasicStyles}
border: 4px dashed rgba(0,0,0,0.1); border: 4px dashed rgba(0,0,0,0.1);
text-align: center; text-align: center;
outline: none;
${({ isDragActive }) => isDragActive && `border: 4px dashed green`}; ${({ isDragActive }) => isDragActive && `border: 4px dashed green`};
@@ -336,3 +337,30 @@ export const PosterLanguageSwitch = styled.div`
} }
`} `}
` `
export const StyledPWAAddButton = styled.div`
border: 2px solid white;
border-radius: 50%;
height: 45px;
width: 45px;
position: relative;
:before,
:after {
content: '';
background: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
:before {
width: 2px;
height: 25px;
}
:after {
width: 25px;
height: 2px;
}
`

View File

@@ -0,0 +1,31 @@
import { CreditCard as CreditCardIcon } from '@material-ui/icons'
import { useTranslation } from 'react-i18next'
import CloseServer from 'components/CloseServer'
import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles'
import AddDialogButton from 'components/Add'
import AboutDialog from 'components/About'
import SettingsDialogButton from 'components/Settings'
import StyledPWAFooter from './style'
export default function PWAFooter({ setIsDonationDialogOpen, isOffline, isLoading }) {
const { t } = useTranslation()
return (
<StyledPWAFooter>
<CloseServer isOffline={isOffline} isLoading={isLoading} />
<StyledMenuButtonWrapper onClick={() => setIsDonationDialogOpen(true)}>
<CreditCardIcon />
<div>{t('Donate')}</div>
</StyledMenuButtonWrapper>
<AddDialogButton isOffline={isOffline} isLoading={isLoading} />
<AboutDialog />
<SettingsDialogButton isOffline={isOffline} isLoading={isLoading} />
</StyledPWAFooter>
)
}

View File

@@ -0,0 +1,21 @@
import { standaloneMedia } from 'style/standaloneMedia'
import styled, { css } from 'styled-components'
export const pwaFooterHeight = 90
export default styled.div`
background: #575757;
color: #fff;
position: fixed;
bottom: 0;
width: 100%;
height: ${pwaFooterHeight}px;
display: none;
${standaloneMedia(css`
display: grid;
grid-template-columns: repeat(5, calc(100% / 5));
justify-items: center;
`)}
`

View File

@@ -0,0 +1,21 @@
export default function IOSShareIcon() {
return (
<svg
version='1.1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
width={23}
x='0px'
y='0px'
viewBox='0 0 1000 1000'
enableBackground='new 0 0 1000 1000'
xmlSpace='preserve'
fill='#005FF2'
>
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g>
<path d='M780,290H640v35h140c19.3,0,35,15.7,35,35v560c0,19.3-15.7,35-35,35H220c-19.2,0-35-15.7-35-35V360c0-19.2,15.7-35,35-35h140v-35H220c-38.7,0-70,31.3-70,70v560c0,38.7,31.3,70,70,70h560c38.7,0,70-31.3,70-70V360C850,321.3,818.7,290,780,290z M372.5,180l110-110.2v552.7c0,9.6,7.9,17.5,17.5,17.5c9.6,0,17.5-7.9,17.5-17.5V69.8l110,110c3.5,3.5,7.9,5,12.5,5s9-1.7,12.5-5c6.8-6.8,6.8-17.9,0-24.7l-140-140c-6.8-6.8-17.9-6.8-24.7,0l-140,140c-6.8,6.8-6.8,17.9,0,24.7C354.5,186.8,365.5,186.8,372.5,180z' />
</g>
</svg>
)
}

View File

@@ -0,0 +1,57 @@
import IconButton from '@material-ui/core/IconButton'
import CloseIcon from '@material-ui/icons/Close'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import IOSShareIcon from './IOSShareIcon'
import { StyledWrapper, StyledHeader, StyledContent } from './style'
export function PWAInstallationGuide() {
const pwaNotificationIsClosed = JSON.parse(localStorage.getItem('pwaNotificationIsClosed'))
const [isOpen, setIsOpen] = useState(!pwaNotificationIsClosed)
const [shouldBeOpened, setShouldBeOpened] = useState(!pwaNotificationIsClosed)
const { t } = useTranslation()
if (!isOpen) return null
return (
<StyledWrapper isOpen={shouldBeOpened}>
<StyledHeader>
<img src='/apple-icon-180.png' width={50} alt='ts-icon' />
{t('PWAGuide.Header')}
<IconButton
size='small'
aria-label='close'
color='inherit'
onClick={() => {
setShouldBeOpened(false)
setTimeout(() => {
setIsOpen(false)
localStorage.setItem('pwaNotificationIsClosed', true)
}, 300)
}}
>
<CloseIcon fontSize='small' />
</IconButton>
</StyledHeader>
<StyledContent>
<p>{t('PWAGuide.Description')}</p>
<p>{t('PWAGuide.VLC')}</p>
<p>
1. {t('PWAGuide.FirstStep')} <IOSShareIcon />
</p>
<p>
2. {t('PWAGuide.SecondStep.Select')} <span>{t('PWAGuide.SecondStep.AddToHomeScreen')}</span>
</p>
</StyledContent>
</StyledWrapper>
)
}

View File

@@ -0,0 +1,59 @@
import styled, { css } from 'styled-components'
export const StyledWrapper = styled.div`
${({ isOpen }) => css`
position: absolute;
bottom: 10px;
left: 50%;
background: #eeeef0;
width: calc(100% - 20px);
z-index: 9999;
border-radius: 10px;
transition: all 0.3s;
color: #000;
${isOpen
? css`
opacity: 1;
transform: translate(-50%, 0);
`
: css`
transform: translate(-50%, 150%);
opacity: 0;
pointer-events: none;
`}
> :not(:last-child) {
border-bottom: 1px solid #dadadc;
}
> * {
padding: 20px;
}
`}
`
export const StyledHeader = styled.div`
display: grid;
grid-auto-flow: column;
grid-template-columns: min-content 1fr;
gap: 20px;
align-items: center;
font-weight: 700;
img {
border-radius: 5px;
}
`
export const StyledContent = styled.div`
> :not(:last-child) {
margin-bottom: 25px;
}
span {
background: #fefcfd;
padding: 5px;
border-radius: 5px;
}
`

View File

@@ -1,7 +1,6 @@
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import { createContext, useEffect, useState } from 'react' import { createContext, useEffect, useState } from 'react'
import Typography from '@material-ui/core/Typography' import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import { import {
Menu as MenuIcon, Menu as MenuIcon,
Close as CloseIcon, Close as CloseIcon,
@@ -19,13 +18,18 @@ import useChangeLanguage from 'utils/useChangeLanguage'
import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles' import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles'
import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components' import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { getTorrents } from 'utils/Utils' import { getTorrents, isStandaloneApp } from 'utils/Utils'
import GlobalStyle from 'style/GlobalStyle' import GlobalStyle from 'style/GlobalStyle'
import { lightTheme, THEME_MODES, useMaterialUITheme } from 'style/materialUISetup'
import getStyledComponentsTheme from 'style/getStyledComponentsTheme'
import checkIsIOS from 'utils/checkIsIOS'
import { AppWrapper, AppHeader, HeaderToggle } from './style' import { AppWrapper, AppHeader, HeaderToggle, StyledIconButton } from './style'
import Sidebar from './Sidebar' import Sidebar from './Sidebar'
import { lightTheme, THEME_MODES, useMaterialUITheme } from '../../style/materialUISetup' import PWAFooter from './PWAFooter'
import getStyledComponentsTheme from '../../style/getStyledComponentsTheme' import { PWAInstallationGuide } from './PWAInstallationGuide'
const snackbarIsClosed = JSON.parse(localStorage.getItem('snackbarIsClosed'))
export const DarkModeContext = createContext() export const DarkModeContext = createContext()
@@ -63,14 +67,9 @@ export default function App() {
<Div100vh> <Div100vh>
<AppWrapper> <AppWrapper>
<AppHeader> <AppHeader>
<IconButton <StyledIconButton edge='start' color='inherit' onClick={() => setIsDrawerOpen(!isDrawerOpen)}>
edge='start'
color='inherit'
onClick={() => setIsDrawerOpen(!isDrawerOpen)}
style={{ marginRight: '6px' }}
>
{isDrawerOpen ? <CloseIcon /> : <MenuIcon />} {isDrawerOpen ? <CloseIcon /> : <MenuIcon />}
</IconButton> </StyledIconButton>
<Typography variant='h6' noWrap> <Typography variant='h6' noWrap>
TorrServer {torrServerVersion} TorrServer {torrServerVersion}
@@ -118,11 +117,17 @@ export default function App() {
<TorrentList isOffline={isOffline} torrents={torrents} isLoading={isLoading} /> <TorrentList isOffline={isOffline} torrents={torrents} isLoading={isLoading} />
<PWAFooter
isOffline={isOffline}
isLoading={isLoading}
setIsDonationDialogOpen={setIsDonationDialogOpen}
/>
<MuiThemeProvider theme={lightTheme}> <MuiThemeProvider theme={lightTheme}>
{isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />} {isDonationDialogOpen && <DonateDialog onClose={() => setIsDonationDialogOpen(false)} />}
</MuiThemeProvider> </MuiThemeProvider>
{!JSON.parse(localStorage.getItem('snackbarIsClosed')) && <DonateSnackbar />} {snackbarIsClosed ? checkIsIOS() && !isStandaloneApp && <PWAInstallationGuide /> : <DonateSnackbar />}
</AppWrapper> </AppWrapper>
</Div100vh> </Div100vh>
</StyledComponentsThemeProvider> </StyledComponentsThemeProvider>

View File

@@ -1,6 +1,10 @@
import { IconButton } from '@material-ui/core'
import { rgba } from 'polished' import { rgba } from 'polished'
import { standaloneMedia } from 'style/standaloneMedia'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { pwaFooterHeight } from './PWAFooter/style'
export const AppWrapper = styled.div` export const AppWrapper = styled.div`
${({ ${({
theme: { theme: {
@@ -15,13 +19,23 @@ export const AppWrapper = styled.div`
grid-template-areas: grid-template-areas:
'head head' 'head head'
'side content'; 'side content';
${standaloneMedia(css`
grid-template-columns: 0 1fr;
grid-template-rows: ${pwaFooterHeight}px 1fr ${pwaFooterHeight}px;
height: 100vh;
`)}
`} `}
` `
export const CenteredGrid = styled.div` export const CenteredGrid = styled.div`
height: 100%;
display: grid; display: grid;
place-items: center; place-items: center;
${standaloneMedia(css`
height: 100vh;
width: 100vw;
`)}
` `
export const AppHeader = styled.div` export const AppHeader = styled.div`
@@ -36,6 +50,15 @@ export const AppHeader = styled.div`
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%); 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 16px; padding: 0 16px;
z-index: 3; z-index: 3;
${standaloneMedia(css`
grid-template-columns: max-content 1fr;
align-items: end;
padding: 7px 16px;
position: fixed;
width: 100%;
height: ${pwaFooterHeight}px;
`)}
`} `}
` `
export const AppSidebarStyle = styled.div` export const AppSidebarStyle = styled.div`
@@ -58,6 +81,10 @@ export const AppSidebarStyle = styled.div`
svg { svg {
fill: ${sidebarFillColor}; fill: ${sidebarFillColor};
} }
${standaloneMedia(css`
display: none;
`)}
`} `}
` `
export const TorrentListWrapper = styled.div` export const TorrentListWrapper = styled.div`
@@ -83,6 +110,11 @@ export const TorrentListWrapper = styled.div`
@media (max-width: 700px) { @media (max-width: 700px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
${standaloneMedia(css`
height: calc(100vh - ${pwaFooterHeight}px);
padding-bottom: 105px;
`)}
` `
export const HeaderToggle = styled.div` export const HeaderToggle = styled.div`
@@ -117,3 +149,11 @@ export const HeaderToggle = styled.div`
} }
`} `}
` `
export const StyledIconButton = styled(IconButton)`
margin-right: 6px;
${standaloneMedia(css`
display: none;
`)}
`

View File

@@ -1,8 +1,11 @@
import { useState } from 'react' import { useState } from 'react'
import { Button, Dialog, DialogActions, DialogTitle, ListItem, ListItemIcon, ListItemText } from '@material-ui/core' import { Button, DialogActions, DialogTitle, ListItemIcon, ListItemText } from '@material-ui/core'
import { StyledDialog, StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles'
import { PowerSettingsNew as PowerSettingsNewIcon } from '@material-ui/icons' import { PowerSettingsNew as PowerSettingsNewIcon } from '@material-ui/icons'
import { shutdownHost } from 'utils/Hosts' import { shutdownHost } from 'utils/Hosts'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { isStandaloneApp } from 'utils/Utils'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
export default function CloseServer({ isOffline, isLoading }) { export default function CloseServer({ isOffline, isLoading }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -10,17 +13,28 @@ export default function CloseServer({ isOffline, isLoading }) {
const closeDialog = () => setOpen(false) const closeDialog = () => setOpen(false)
const openDialog = () => setOpen(true) const openDialog = () => setOpen(true)
const ref = useOnStandaloneAppOutsideClick(closeDialog)
return ( return (
<> <>
<ListItem disabled={isOffline || isLoading} button key={t('CloseServer')} onClick={openDialog}> <StyledMenuButtonWrapper disabled={isOffline || isLoading} button key={t('CloseServer')} onClick={openDialog}>
<ListItemIcon> {isStandaloneApp ? (
<PowerSettingsNewIcon /> <>
</ListItemIcon> <PowerSettingsNewIcon />
<div>{t('TurnOff')}</div>
</>
) : (
<>
<ListItemIcon>
<PowerSettingsNewIcon />
</ListItemIcon>
<ListItemText primary={t('CloseServer')} /> <ListItemText primary={t('CloseServer')} />
</ListItem> </>
)}
</StyledMenuButtonWrapper>
<Dialog open={open} onClose={closeDialog}> <StyledDialog open={open} onClose={closeDialog} ref={ref}>
<DialogTitle>{t('CloseServer?')}</DialogTitle> <DialogTitle>{t('CloseServer?')}</DialogTitle>
<DialogActions> <DialogActions>
<Button variant='outlined' onClick={closeDialog} color='secondary'> <Button variant='outlined' onClick={closeDialog} color='secondary'>
@@ -39,7 +53,7 @@ export default function CloseServer({ isOffline, isLoading }) {
{t('TurnOff')} {t('TurnOff')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </StyledDialog>
</> </>
) )
} }

View File

@@ -1,9 +1,10 @@
import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core' import { AppBar, IconButton, makeStyles, Toolbar, Typography } from '@material-ui/core'
import CloseIcon from '@material-ui/icons/Close' import CloseIcon from '@material-ui/icons/Close'
import { ArrowBack } from '@material-ui/icons' import { ArrowBack } from '@material-ui/icons'
import { isStandaloneApp } from 'utils/Utils'
const useStyles = makeStyles({ const useStyles = makeStyles({
appBar: { position: 'relative' }, appBar: { position: 'relative', ...(isStandaloneApp && { paddingTop: '30px' }) },
title: { marginLeft: '5px', flex: 1 }, title: { marginLeft: '5px', flex: 1 },
}) })

View File

@@ -28,6 +28,8 @@ const Table = memo(
// if files in list is more then 1 and no season text detected by ptt.parse, show full name // if files in list is more then 1 and no season text detected by ptt.parse, show full name
const shouldDisplayFullFileName = playableFileList.length > 1 && !fileHasEpisodeText const shouldDisplayFullFileName = playableFileList.length > 1 && !fileHasEpisodeText
const isVlcUsed = JSON.parse(localStorage.getItem('isVlcUsed')) ?? true
return !playableFileList?.length ? ( return !playableFileList?.length ? (
'No playable files in this torrent' 'No playable files in this torrent'
) : ( ) : (
@@ -133,11 +135,19 @@ const Table = memo(
{t('Preload')} {t('Preload')}
</Button> </Button>
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'> {isVlcUsed ? (
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'> <a style={{ textDecoration: 'none' }} href={`vlc://${link}`}>
{t('OpenLink')} <Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
</Button> VLC
</a> </Button>
</a>
) : (
<a style={{ textDecoration: 'none' }} href={link} target='_blank' rel='noreferrer'>
<Button style={{ width: '100%' }} variant='outlined' color='primary' size='small'>
{t('OpenLink')}
</Button>
</a>
)}
<CopyToClipboard text={link}> <CopyToClipboard text={link}>
<Button variant='outlined' color='primary' size='small'> <Button variant='outlined' color='primary' size='small'>

View File

@@ -1,5 +1,4 @@
import ListItem from '@material-ui/core/ListItem' import ListItem from '@material-ui/core/ListItem'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle' import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent' import DialogContent from '@material-ui/core/DialogContent'
import DialogActions from '@material-ui/core/DialogActions' import DialogActions from '@material-ui/core/DialogActions'
@@ -7,15 +6,18 @@ import List from '@material-ui/core/List'
import ButtonGroup from '@material-ui/core/ButtonGroup' import ButtonGroup from '@material-ui/core/ButtonGroup'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledDialog } from 'style/CustomMaterialUiStyles'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
const donateFrame = const donateFrame =
'<iframe src="https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=TorrServer Donate&targets-hint=&default-sum=200&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&comment=on&hint=&successURL=&quickpay=shop&account=410013733697114" width="320" height="320" frameborder="0" allowtransparency="true" scrolling="no"></iframe>' '<iframe src="https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=TorrServer Donate&targets-hint=&default-sum=200&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&comment=on&hint=&successURL=&quickpay=shop&account=410013733697114" width="320" height="320" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
export default function DonateDialog({ onClose }) { export default function DonateDialog({ onClose }) {
const { t } = useTranslation() const { t } = useTranslation()
const ref = useOnStandaloneAppOutsideClick(onClose)
return ( return (
<Dialog open onClose={onClose} aria-labelledby='form-dialog-title' fullWidth maxWidth='xs'> <StyledDialog open onClose={onClose} aria-labelledby='form-dialog-title' fullWidth maxWidth='xs' ref={ref}>
<DialogTitle id='form-dialog-title'>{t('Donate')}</DialogTitle> <DialogTitle id='form-dialog-title'>{t('Donate')}</DialogTitle>
<DialogContent> <DialogContent>
<List> <List>
@@ -38,6 +40,6 @@ export default function DonateDialog({ onClose }) {
Ok Ok
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </StyledDialog>
) )
} }

View File

@@ -5,9 +5,15 @@ import IconButton from '@material-ui/core/IconButton'
import CreditCardIcon from '@material-ui/icons/CreditCard' import CreditCardIcon from '@material-ui/icons/CreditCard'
import CloseIcon from '@material-ui/icons/Close' import CloseIcon from '@material-ui/icons/Close'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { standaloneMedia } from 'style/standaloneMedia'
import DonateDialog from './DonateDialog' import DonateDialog from './DonateDialog'
const StyledSnackbar = styled(Snackbar)`
${standaloneMedia('margin-bottom: 90px')};
`
export default function DonateSnackbar() { export default function DonateSnackbar() {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -22,7 +28,7 @@ export default function DonateSnackbar() {
<> <>
{open && <DonateDialog onClose={() => setOpen(false)} />} {open && <DonateDialog onClose={() => setOpen(false)} />}
<Snackbar <StyledSnackbar
anchorOrigin={{ anchorOrigin={{
vertical: 'bottom', vertical: 'bottom',
horizontal: 'center', horizontal: 'center',

View File

@@ -0,0 +1,20 @@
import { FormControlLabel, Switch } from '@material-ui/core'
import { useTranslation } from 'react-i18next'
import { SecondarySettingsContent, SettingSectionLabel } from './style'
export default function MobileAppSettings({ isVlcUsed, setIsVlcUsed }) {
const { t } = useTranslation()
return (
<SecondarySettingsContent>
<SettingSectionLabel>{t('SettingsDialog.MobileAppSettings')}</SettingSectionLabel>
<FormControlLabel
control={<Switch checked={isVlcUsed} onChange={() => setIsVlcUsed(prev => !prev)} color='secondary' />}
label={t('SettingsDialog.UseVLC')}
labelPlacement='start'
/>
</SecondarySettingsContent>
)
}

View File

@@ -1,5 +1,4 @@
import axios from 'axios' import axios from 'axios'
import Dialog from '@material-ui/core/Dialog'
import Button from '@material-ui/core/Button' import Button from '@material-ui/core/Button'
import Checkbox from '@material-ui/core/Checkbox' import Checkbox from '@material-ui/core/Checkbox'
import { FormControlLabel, useMediaQuery, useTheme } from '@material-ui/core' import { FormControlLabel, useMediaQuery, useTheme } from '@material-ui/core'
@@ -11,12 +10,16 @@ import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab' import Tab from '@material-ui/core/Tab'
import SwipeableViews from 'react-swipeable-views' import SwipeableViews from 'react-swipeable-views'
import CircularProgress from '@material-ui/core/CircularProgress' import CircularProgress from '@material-ui/core/CircularProgress'
import { StyledDialog } from 'style/CustomMaterialUiStyles'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
import { isStandaloneApp } from 'utils/Utils'
import { SettingsHeader, FooterSection, Content } from './style' import { SettingsHeader, FooterSection, Content } from './style'
import defaultSettings from './defaultSettings' import defaultSettings from './defaultSettings'
import { a11yProps, TabPanel } from './tabComponents' import { a11yProps, TabPanel } from './tabComponents'
import PrimarySettingsComponent from './PrimarySettingsComponent' import PrimarySettingsComponent from './PrimarySettingsComponent'
import SecondarySettingsComponent from './SecondarySettingsComponent' import SecondarySettingsComponent from './SecondarySettingsComponent'
import MobileAppSettings from './MobileAppSettings'
export default function SettingsDialog({ handleClose }) { export default function SettingsDialog({ handleClose }) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -29,6 +32,7 @@ export default function SettingsDialog({ handleClose }) {
const [cachePercentage, setCachePercentage] = useState(40) const [cachePercentage, setCachePercentage] = useState(40)
const [preloadCachePercentage, setPreloadCachePercentage] = useState(0) const [preloadCachePercentage, setPreloadCachePercentage] = useState(0)
const [isProMode, setIsProMode] = useState(JSON.parse(localStorage.getItem('isProMode')) || false) const [isProMode, setIsProMode] = useState(JSON.parse(localStorage.getItem('isProMode')) || false)
const [isVlcUsed, setIsVlcUsed] = useState(JSON.parse(localStorage.getItem('isVlcUsed')) ?? true)
useEffect(() => { useEffect(() => {
axios.post(settingsHost(), { action: 'get' }).then(({ data }) => { axios.post(settingsHost(), { action: 'get' }).then(({ data }) => {
@@ -36,6 +40,8 @@ export default function SettingsDialog({ handleClose }) {
}) })
}, []) }, [])
const ref = useOnStandaloneAppOutsideClick(handleClose)
const handleSave = () => { const handleSave = () => {
handleClose() handleClose()
const sets = JSON.parse(JSON.stringify(settings)) const sets = JSON.parse(JSON.stringify(settings))
@@ -43,6 +49,7 @@ export default function SettingsDialog({ handleClose }) {
sets.ReaderReadAHead = cachePercentage sets.ReaderReadAHead = cachePercentage
sets.PreloadCache = preloadCachePercentage sets.PreloadCache = preloadCachePercentage
axios.post(settingsHost(), { action: 'set', sets }) axios.post(settingsHost(), { action: 'set', sets })
localStorage.setItem('isVlcUsed', isVlcUsed)
} }
const inputForm = ({ target: { type, value, checked, id } }) => { const inputForm = ({ target: { type, value, checked, id } }) => {
@@ -82,7 +89,7 @@ export default function SettingsDialog({ handleClose }) {
const handleChangeIndex = index => setSelectedTab(index) const handleChangeIndex = index => setSelectedTab(index)
return ( return (
<Dialog open onClose={handleClose} fullScreen={fullScreen} fullWidth maxWidth='md'> <StyledDialog open onClose={handleClose} fullScreen={fullScreen} fullWidth maxWidth='md' ref={ref}>
<SettingsHeader> <SettingsHeader>
<div>{t('SettingsDialog.Settings')}</div> <div>{t('SettingsDialog.Settings')}</div>
<FormControlLabel <FormControlLabel
@@ -121,6 +128,8 @@ export default function SettingsDialog({ handleClose }) {
} }
{...a11yProps(1)} {...a11yProps(1)}
/> />
{isStandaloneApp && <Tab label={t('SettingsDialog.Tabs.App')} {...a11yProps(2)} />}
</Tabs> </Tabs>
</AppBar> </AppBar>
@@ -150,6 +159,12 @@ export default function SettingsDialog({ handleClose }) {
<TabPanel value={selectedTab} index={1} dir={direction}> <TabPanel value={selectedTab} index={1} dir={direction}>
<SecondarySettingsComponent settings={settings} inputForm={inputForm} /> <SecondarySettingsComponent settings={settings} inputForm={inputForm} />
</TabPanel> </TabPanel>
{isStandaloneApp && (
<TabPanel value={selectedTab} index={2} dir={direction}>
<MobileAppSettings isVlcUsed={isVlcUsed} setIsVlcUsed={setIsVlcUsed} />
</TabPanel>
)}
</SwipeableViews> </SwipeableViews>
</> </>
) : ( ) : (
@@ -179,6 +194,6 @@ export default function SettingsDialog({ handleClose }) {
{t('Save')} {t('Save')}
</Button> </Button>
</FooterSection> </FooterSection>
</Dialog> </StyledDialog>
) )
} }

View File

@@ -1,9 +1,10 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon' import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText' import ListItemText from '@material-ui/core/ListItemText'
import { useState } from 'react' import { useState } from 'react'
import SettingsIcon from '@material-ui/icons/Settings' import SettingsIcon from '@material-ui/icons/Settings'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledMenuButtonWrapper } from 'style/CustomMaterialUiStyles'
import { isStandaloneApp } from 'utils/Utils'
import SettingsDialog from './SettingsDialog' import SettingsDialog from './SettingsDialog'
@@ -16,12 +17,22 @@ export default function SettingsDialogButton({ isOffline, isLoading }) {
return ( return (
<div> <div>
<ListItem disabled={isOffline || isLoading} button onClick={handleClickOpen}> <StyledMenuButtonWrapper disabled={isOffline || isLoading} button onClick={handleClickOpen}>
<ListItemIcon> {isStandaloneApp ? (
<SettingsIcon /> <>
</ListItemIcon> <SettingsIcon />
<ListItemText primary={t('SettingsDialog.Settings')} /> <div>{t('SettingsDialog.Settings')}</div>
</ListItem> </>
) : (
<>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary={t('SettingsDialog.Settings')} />
</>
)}
</StyledMenuButtonWrapper>
{isDialogOpen && <SettingsDialog handleClose={handleClose} />} {isDialogOpen && <SettingsDialog handleClose={handleClose} />}
</div> </div>

View File

@@ -1,11 +1,11 @@
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { mainColors } from 'style/colors' import { mainColors } from 'style/colors'
import { Header } from 'style/DialogStyles' import { StyledHeader } from 'style/CustomMaterialUiStyles'
export const cacheBeforeReaderColor = '#b3dfc9' export const cacheBeforeReaderColor = '#b3dfc9'
export const cacheAfterReaderColor = mainColors.light.primary export const cacheAfterReaderColor = mainColors.light.primary
export const SettingsHeader = styled(Header)` export const SettingsHeader = styled(StyledHeader)`
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
align-items: center; align-items: center;

View File

@@ -16,6 +16,8 @@ import axios from 'axios'
import ptt from 'parse-torrent-title' import ptt from 'parse-torrent-title'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AddDialog from 'components/Add/AddDialog' import AddDialog from 'components/Add/AddDialog'
import { StyledDialog } from 'style/CustomMaterialUiStyles'
import useOnStandaloneAppOutsideClick from 'utils/useOnStandaloneAppOutsideClick'
import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style' import { StyledButton, TorrentCard, TorrentCardButtons, TorrentCardDescription, TorrentCardPoster } from './style'
@@ -61,6 +63,8 @@ const Torrent = ({ torrent }) => {
const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(parsedTitle || 'file')}.m3u?link=${hash}&m3u` const fullPlaylistLink = `${playlistTorrHost()}/${encodeURIComponent(parsedTitle || 'file')}.m3u?link=${hash}&m3u`
const detailedInfoDialogRef = useOnStandaloneAppOutsideClick(closeDetailedInfo)
return ( return (
<> <>
<TorrentCard> <TorrentCard>
@@ -121,16 +125,17 @@ const Torrent = ({ torrent }) => {
</TorrentCardDescription> </TorrentCardDescription>
</TorrentCard> </TorrentCard>
<Dialog <StyledDialog
open={isDetailedInfoOpened} open={isDetailedInfoOpened}
onClose={closeDetailedInfo} onClose={closeDetailedInfo}
fullScreen={fullScreen} fullScreen={fullScreen}
fullWidth fullWidth
maxWidth='xl' maxWidth='xl'
TransitionComponent={Transition} TransitionComponent={Transition}
ref={detailedInfoDialogRef}
> >
<DialogTorrentDetailsContent closeDialog={closeDetailedInfo} torrent={torrent} /> <DialogTorrentDetailsContent closeDialog={closeDetailedInfo} torrent={torrent} />
</Dialog> </StyledDialog>
<Dialog open={isDeleteTorrentOpened} onClose={closeDeleteTorrentAlert}> <Dialog open={isDeleteTorrentOpened} onClose={closeDeleteTorrentAlert}>
<DialogTitle>{t('DeleteTorrent?')}</DialogTitle> <DialogTitle>{t('DeleteTorrent?')}</DialogTitle>

View File

@@ -1,7 +1,12 @@
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
export default styled.div` export default styled.div`
${({ isButton }) => css` ${({
isButton,
theme: {
addDialog: { notificationSuccessBGColor, languageSwitchBGColor },
},
}) => css`
display: grid; display: grid;
place-items: center; place-items: center;
padding: 20px 40px; padding: 20px 40px;
@@ -9,12 +14,12 @@ export default styled.div`
${isButton && ${isButton &&
css` css`
background: #88cdaa; background: ${notificationSuccessBGColor};
transition: 0.2s; transition: 0.2s;
cursor: pointer; cursor: pointer;
:hover { :hover {
background: #74c39c; background: ${languageSwitchBGColor};
} }
`} `}

View File

@@ -75,6 +75,16 @@
"Playlist": "Playlist", "Playlist": "Playlist",
"Preload": "Preload", "Preload": "Preload",
"ProjectSource": "Project page", "ProjectSource": "Project page",
"PWAGuide": {
"Header": "Install application",
"Description": "Install the app on your device to easily access it anytime. No app store. No download.",
"VLC": "VLC button will be added to open video instantly on the phone",
"FirstStep": "Tap on",
"SecondStep": {
"Select": "Select",
"AddToHomeScreen":"Add to Home Screen"
}
},
"Releases": "TorrServer Releases", "Releases": "TorrServer Releases",
"RemoveAll": "Remove All", "RemoveAll": "Remove All",
"RemoveViews": "Remove View States", "RemoveViews": "Remove View States",
@@ -88,6 +98,7 @@
"SettingsDialog": { "SettingsDialog": {
"AddRetrackers": "Add retrackers", "AddRetrackers": "Add retrackers",
"AdditionalSettings": "Additional Settings", "AdditionalSettings": "Additional Settings",
"MobileAppSettings": "Mobile app settings",
"CacheBeforeReaderDesc": "from cache will be saved before currently played frame", "CacheBeforeReaderDesc": "from cache will be saved before currently played frame",
"CacheAfterReaderDesc": "from cache will be loaded after currently played frame", "CacheAfterReaderDesc": "from cache will be loaded after currently played frame",
"CacheSize": "Cache Size", "CacheSize": "Cache Size",
@@ -125,8 +136,10 @@
"Tabs": { "Tabs": {
"Main": "Main", "Main": "Main",
"Additional": "Additional", "Additional": "Additional",
"AdditionalDisabled": "(enable PRO mode)" "AdditionalDisabled": "(enable PRO mode)",
} "App": "App"
},
"UseVLC": "Prompt to open video in VLC"
}, },
"Size": "Size", "Size": "Size",
"SpecialThanks": "Special Thanks", "SpecialThanks": "Special Thanks",

View File

@@ -75,6 +75,16 @@
"Playlist": "Плейлист", "Playlist": "Плейлист",
"Preload": "Предзагр.", "Preload": "Предзагр.",
"ProjectSource": "Сайт проекта", "ProjectSource": "Сайт проекта",
"PWAGuide": {
"Header": "Установить приложение",
"Description": "Установите приложение на ваше устройство для быстрого доступа в любой момент. Без AppStore. Без загрузки.",
"VLC": "VLC кнопка будет добавлена для мгновенного воспроизведения на телефоне",
"FirstStep": "Нажмите на",
"SecondStep": {
"Select": "Выбирите",
"AddToHomeScreen":"На экран «Домой»"
}
},
"Releases": "Релизы TorrServer", "Releases": "Релизы TorrServer",
"RemoveAll": "Удалить все", "RemoveAll": "Удалить все",
"RemoveViews": "Очистить просмотры", "RemoveViews": "Очистить просмотры",
@@ -88,6 +98,7 @@
"SettingsDialog": { "SettingsDialog": {
"AddRetrackers": "Добавлять", "AddRetrackers": "Добавлять",
"AdditionalSettings": "Дополнительные настройки", "AdditionalSettings": "Дополнительные настройки",
"MobileAppSettings": "Настройки моб. приложения",
"CacheBeforeReaderDesc": "от кеша будет оставаться позади воспроизводимого кадра", "CacheBeforeReaderDesc": "от кеша будет оставаться позади воспроизводимого кадра",
"CacheAfterReaderDesc": "кеша будет спереди от воспроизводимого кадра", "CacheAfterReaderDesc": "кеша будет спереди от воспроизводимого кадра",
"CacheSize": "Размер кеша", "CacheSize": "Размер кеша",
@@ -125,8 +136,10 @@
"Tabs": { "Tabs": {
"Main": "Основные", "Main": "Основные",
"Additional": "Дополнительные", "Additional": "Дополнительные",
"AdditionalDisabled": "(включите ПРО-режим)" "AdditionalDisabled": "(включите ПРО-режим)",
} "App": "Приложение"
},
"UseVLC": "Предлагать открыть видео в VLC"
}, },
"Size": "Размер", "Size": "Размер",
"SpecialThanks": "Отдельное спасибо", "SpecialThanks": "Отдельное спасибо",

View File

@@ -75,6 +75,16 @@
"Playlist": "Плейлист", "Playlist": "Плейлист",
"Preload": "Передзав.", "Preload": "Передзав.",
"ProjectSource": "Сайт проекту", "ProjectSource": "Сайт проекту",
"PWAGuide": {
"Header": "Встановити додаток",
"Description": "Встановіть програму на свій пристрій, щоб легко отримати до неї доступ у будь-який час. Немає магазину додатків. Немає завантаження.",
"VLC": "Кнопка VLC буде додана, щоб миттєво відкривати відео на телефоні",
"FirstStep": "Торкніться",
"SecondStep": {
"Select": "Виберіть",
"AddToHomeScreen":"Додати на головний екран"
}
},
"Releases": "Релізи TorrServer", "Releases": "Релізи TorrServer",
"RemoveAll": "Видалити все", "RemoveAll": "Видалити все",
"RemoveViews": "Видалити перегляди", "RemoveViews": "Видалити перегляди",
@@ -88,6 +98,7 @@
"SettingsDialog": { "SettingsDialog": {
"AddRetrackers": "Додавати", "AddRetrackers": "Додавати",
"AdditionalSettings": "Додаткові налаштування", "AdditionalSettings": "Додаткові налаштування",
"MobileAppSettings": "Установки моб. програми",
"CacheBeforeReaderDesc": "з кешу буде збережено до поточного відтворюваного кадру", "CacheBeforeReaderDesc": "з кешу буде збережено до поточного відтворюваного кадру",
"CacheAfterReaderDesc": "з кешу буде завантажено після поточно відтвореного кадру", "CacheAfterReaderDesc": "з кешу буде завантажено після поточно відтвореного кадру",
"CacheSize": "Размір кешу", "CacheSize": "Размір кешу",
@@ -125,8 +136,10 @@
"Tabs": { "Tabs": {
"Main": "Основні", "Main": "Основні",
"Additional": "Додаткові", "Additional": "Додаткові",
"AdditionalDisabled": "(включіть ПРО-режим)" "AdditionalDisabled": "(включіть ПРО-режим)",
} "App": "Додаток"
},
"UseVLC": "Пропонувати відкрити відео у VLC"
}, },
"Size": "Розмір", "Size": "Розмір",
"SpecialThanks": "Окрема подяка", "SpecialThanks": "Окрема подяка",

View File

@@ -0,0 +1,38 @@
import { ListItem } from '@material-ui/core'
import Dialog from '@material-ui/core/Dialog'
import { pwaFooterHeight } from 'components/App/PWAFooter/style'
import styled, { css } from 'styled-components'
import { Header } from 'style/DialogStyles'
import { isStandaloneApp } from 'utils/Utils'
import { standaloneMedia } from './standaloneMedia'
export const StyledMenuButtonWrapper = styled(ListItem).attrs({ button: true })`
${standaloneMedia(css`
width: 100%;
height: 60px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 10px;
`)}
`
export const StyledDialog = styled(Dialog).attrs({
...(isStandaloneApp && { hideBackdrop: true, transitionDuration: 0 }),
})`
${standaloneMedia(css`
margin-bottom: ${pwaFooterHeight}px;
.MuiDialog-container .MuiPaper-root {
box-shadow: none;
}
`)}
`
export const StyledHeader = styled(Header)`
${standaloneMedia(css`
padding-top: 47px;
`)}
`

View File

@@ -1,4 +1,6 @@
import { createGlobalStyle } from 'styled-components' import { createGlobalStyle, css } from 'styled-components'
import { standaloneMedia } from './standaloneMedia'
export default createGlobalStyle` export default createGlobalStyle`
*, *,
@@ -15,6 +17,12 @@ export default createGlobalStyle`
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
letter-spacing: -0.1px; letter-spacing: -0.1px;
-webkit-tap-highlight-color: transparent;
${standaloneMedia(css`
height: 100vh;
`)}
} }
button { button {

View File

@@ -1,4 +1,4 @@
import { createMuiTheme, useMediaQuery } from '@material-ui/core' import { createTheme, useMediaQuery } from '@material-ui/core'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { mainColors } from './colors' import { mainColors } from './colors'
@@ -7,7 +7,7 @@ export const THEME_MODES = { LIGHT: 'light', DARK: 'dark', AUTO: 'auto' }
const typography = { fontFamily: 'Open Sans, sans-serif' } const typography = { fontFamily: 'Open Sans, sans-serif' }
export const darkTheme = createMuiTheme({ export const darkTheme = createTheme({
typography, typography,
palette: { palette: {
type: THEME_MODES.DARK, type: THEME_MODES.DARK,
@@ -15,7 +15,7 @@ export const darkTheme = createMuiTheme({
secondary: { main: mainColors.dark.secondary }, secondary: { main: mainColors.dark.secondary },
}, },
}) })
export const lightTheme = createMuiTheme({ export const lightTheme = createTheme({
typography, typography,
palette: { palette: {
type: THEME_MODES.LIGHT, type: THEME_MODES.LIGHT,
@@ -45,7 +45,7 @@ export const useMaterialUITheme = () => {
const muiTheme = useMemo( const muiTheme = useMemo(
() => () =>
createMuiTheme({ createTheme({
typography, typography,
palette: { palette: {
type: theme, type: theme,

View File

@@ -0,0 +1,7 @@
import { css } from 'styled-components'
export const standaloneMedia = styles => css`
@media screen and (display-mode: standalone) {
${styles};
}
`

View File

@@ -65,3 +65,5 @@ export const getTorrents = async () => {
throw new Error(null) throw new Error(null)
} }
} }
export const isStandaloneApp = window.matchMedia('screen and (display-mode: standalone)').matches

View File

@@ -0,0 +1,5 @@
export default () => {
if (typeof window === `undefined` || typeof navigator === `undefined`) return false
return /iPhone|iPad|iPod/i.test(navigator.userAgent || navigator.vendor)
}

View File

@@ -0,0 +1,24 @@
import { useEffect, useRef } from 'react'
import { isStandaloneApp } from 'utils/Utils'
export default function useOnStandaloneAppOutsideClick(onClickOutside) {
const ref = useRef()
useEffect(() => {
if (!isStandaloneApp) return
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
onClickOutside && onClickOutside()
}
}
document.addEventListener('click', handleClickOutside, true)
return () => {
document.removeEventListener('click', handleClickOutside, true)
}
})
return ref
}